--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit b2c620eb201e187cef8dd446b858659593b37aca
Parents : 356aea3
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-27T11:15:25-05:00
refactor(components): standardize z-index values and improve class naming conventions across multiple components
Changes
86 files changed, 2876 insertions(+), 1131 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index f276975e..17161b50 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -28,7 +28,7 @@
<template v-else>
<div
- class="sticky top-0 z-[100] flex bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-sm transition-colors overflow-x-hidden"
+ class="sticky top-0 z-100 flex bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-xs transition-colors overflow-x-hidden"
>
<div class="flex w-full px-2 sm:px-4 overflow-x-auto no-scrollbar">
<button
@@ -93,7 +93,7 @@
</button>
<button type="button" class="hidden sm:flex rounded-full" @click="syncPropagationNode">
<span
- class="flex text-gray-800 dark:text-zinc-100 bg-white dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 hover:border-blue-400 dark:hover:border-blue-400/60 px-3 py-1.5 rounded-full shadow-sm transition"
+ class="flex text-gray-800 dark:text-zinc-100 bg-white dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 hover:border-blue-400 dark:hover:border-blue-400/60 px-3 py-1.5 rounded-full shadow-xs transition"
>
<MaterialDesignIcon
icon-name="refresh"
@@ -107,7 +107,7 @@
</button>
<button type="button" class="hidden sm:flex rounded-full" @click="composeNewMessage">
<span
- class="flex rounded-full border border-zinc-800 bg-zinc-900 px-3 py-1.5 text-white shadow-sm transition hover:bg-zinc-800 dark:border-zinc-400 dark:bg-zinc-200 dark:text-zinc-900 dark:hover:bg-white"
+ class="flex rounded-full border border-zinc-800 bg-zinc-900 px-3 py-1.5 text-white shadow-xs transition hover:bg-zinc-800 dark:border-zinc-400 dark:bg-zinc-200 dark:text-zinc-900 dark:hover:bg-white"
>
<span>
<MaterialDesignIcon icon-name="email" class="w-6 h-6" />
@@ -130,13 +130,13 @@
<!-- sidebar backdrop for mobile -->
<div
v-if="isSidebarOpen"
- class="fixed inset-0 z-[65] bg-black/20 backdrop-blur-sm sm:hidden"
+ class="fixed inset-0 z-65 bg-black/20 backdrop-blur-xs sm:hidden"
@click="isSidebarOpen = false"
></div>
<!-- sidebar -->
<div
- class="fixed inset-y-0 left-0 z-[70] transform transition-all duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
+ class="fixed inset-y-0 left-0 z-70 transform transition-all duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
:class="[
isSidebarOpen ? 'translate-x-0' : '-translate-x-full',
isSidebarCollapsed ? 'w-16' : 'w-80 md:max-lg:w-64 lg:w-80',
@@ -371,13 +371,17 @@
/>
</RouterLink>
</div>
- <div v-if="!isSidebarCollapsed" class="my-auto dark:text-white truncate">
- {{ $t("app.my_identity") }}
+ <div
+ v-if="!isSidebarCollapsed"
+ class="my-auto min-w-0 flex-1 dark:text-white truncate"
+ :title="identitySidebarLabel"
+ >
+ {{ identitySidebarLabel }}
</div>
<div v-if="!isSidebarCollapsed" class="my-auto ml-auto shrink-0">
<button
type="button"
- class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
+ class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-xs hover:bg-gray-400 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
@click.stop="saveIdentitySettings"
>
{{ $t("common.save") }}
@@ -386,7 +390,7 @@
</div>
<div
v-if="isShowingMyIdentitySection && !isSidebarCollapsed"
- class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
+ class="divide-y divide-gray-200 text-gray-900 border-t border-gray-200 dark:divide-zinc-800 dark:text-zinc-200 dark:border-zinc-800"
>
<div class="p-2">
<input
@@ -408,17 +412,17 @@
</div>
<div class="p-2 dark:border-zinc-900 overflow-hidden text-xs">
<div>{{ $t("app.lxmf_address") }}</div>
- <div
- class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono cursor-pointer"
- :title="config.lxmf_address_hash"
- @click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
- >
- {{ config.lxmf_address_hash }}
- </div>
- <div class="flex items-center justify-end pt-1">
+ <div class="flex min-w-0 items-center gap-1">
+ <div
+ class="min-w-0 flex-1 text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono cursor-pointer"
+ :title="config.lxmf_address_hash"
+ @click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
+ >
+ {{ config.lxmf_address_hash }}
+ </div>
<button
type="button"
- class="p-1 rounded-lg text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
+ class="shrink-0 rounded-lg p-1 text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
:title="$t('app.show_qr')"
@click.stop="openLxmfQr"
>
@@ -436,18 +440,25 @@
>
<div
class="flex text-gray-700 p-3 cursor-pointer dark:text-white"
+ data-testid="sidebar-announce-header"
@click="isShowingAnnounceSection = !isShowingAnnounceSection"
>
- <div class="my-auto mr-2 shrink-0">
+ <button
+ type="button"
+ class="my-auto mr-2 flex shrink-0 items-center justify-center rounded-md border-0 bg-transparent p-0 text-inherit cursor-pointer"
+ :title="$t('app.announce_now')"
+ data-testid="sidebar-announce-radio"
+ @click.stop="sendAnnounce"
+ >
<MaterialDesignIcon icon-name="radio" class="size-6" />
- </div>
+ </button>
<div v-if="!isSidebarCollapsed" class="my-auto truncate">
{{ $t("app.announce") }}
</div>
<div v-if="!isSidebarCollapsed" class="ml-auto shrink-0">
<button
type="button"
- class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
+ class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-xs hover:bg-gray-400 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
@click.stop="sendAnnounce"
>
{{ $t("app.announce_now") }}
@@ -456,7 +467,7 @@
</div>
<div
v-if="isShowingAnnounceSection && !isSidebarCollapsed"
- class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
+ class="divide-y divide-gray-200 text-gray-900 border-t border-gray-200 dark:divide-zinc-800 dark:text-zinc-200 dark:border-zinc-800"
>
<div class="p-2 dark:border-zinc-800">
<select
@@ -522,7 +533,7 @@
<!-- LXMF QR modal -->
<div
v-if="showLxmfQr"
- class="fixed inset-0 z-[190] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
+ class="fixed inset-0 z-190 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
@click.self="showLxmfQr = false"
>
<div class="w-full max-w-sm bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden">
@@ -547,7 +558,7 @@
</div>
<div
v-if="config?.lxmf_address_hash"
- class="text-xs font-mono text-gray-700 dark:text-zinc-200 text-center break-words"
+ class="text-xs font-mono text-gray-700 dark:text-zinc-200 text-center wrap-break-word"
>
{{ getMyIdentityUri() }}
</div>
@@ -568,7 +579,7 @@
<transition name="fade-blur">
<div
v-if="isSwitchingIdentity"
- class="fixed inset-0 z-[200] flex items-center justify-center bg-slate-900/45 dark:bg-black/55 backdrop-blur-sm px-4"
+ class="fixed inset-0 z-200 flex items-center justify-center bg-slate-900/45 dark:bg-black/55 backdrop-blur-xs px-4"
role="status"
aria-live="polite"
>
@@ -631,6 +642,7 @@ import TutorialModal from "./TutorialModal.vue";
import AppShellBanners from "./layout/AppShellBanners.vue";
import KeyboardShortcuts from "../js/KeyboardShortcuts";
import ElectronUtils from "../js/ElectronUtils";
+import { postRequestPath } from "../js/reticulumPathfinding.js";
import ToneGenerator from "../js/ToneGenerator";
import logoUrl from "../assets/images/logo.png";
@@ -738,6 +750,11 @@ export default {
showWsDisconnectedBanner() {
return this.shellRunning && this.wsDisconnected && this.$route?.name !== "auth";
},
+ identitySidebarLabel() {
+ const raw = this.displayName;
+ const name = raw != null && String(raw).trim() !== "" ? String(raw).trim() : "";
+ return name || this.$t("app.my_identity");
+ },
shellCanvasStyle() {
const raw = Number(this.config?.ui_transparency ?? 0);
const t = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : 0;
@@ -796,6 +813,10 @@ export default {
if (v === "true" || v === "false") {
GlobalState.detailedOutboundSendStatus = v === "true";
}
+ const tg = localStorage.getItem("meshchatx_message_timestamp_grouping_enabled");
+ if (tg === "true" || tg === "false") {
+ GlobalState.messageTimestampGroupingEnabled = tg === "true";
+ }
} catch {
// ignore
}
@@ -1424,7 +1445,7 @@ export default {
try {
const preferredHash = this.config?.lxmf_preferred_propagation_node_destination_hash;
if (preferredHash) {
- await window.api.post(`/api/v1/destination/${preferredHash}/request-path`);
+ await postRequestPath(window.api, preferredHash);
}
await window.api.get("/api/v1/lxmf/propagation-node/sync");
} catch (e) {
@@ -1821,8 +1842,9 @@ export default {
</script>
<style>
+@reference "../style.css";
.banished-overlay {
- @apply absolute inset-0 z-[100] flex items-center justify-center overflow-hidden pointer-events-none rounded-[inherit];
+ @apply absolute inset-0 z-100 flex items-center justify-center overflow-hidden pointer-events-none rounded-[inherit];
background: rgba(220, 38, 38, 0.12);
backdrop-filter: blur(3px) saturate(180%);
}
diff --git a/meshchatx/src/frontend/components/CardStack.vue b/meshchatx/src/frontend/components/CardStack.vue
index ccd3bc10..35578159 100644
--- a/meshchatx/src/frontend/components/CardStack.vue
+++ b/meshchatx/src/frontend/components/CardStack.vue
@@ -33,19 +33,19 @@
</div>
<!-- Controls -->
- <div v-if="items.length > 1" class="absolute -bottom-2 right-0 flex items-center gap-2 z-[60]">
+ <div v-if="items.length > 1" class="absolute -bottom-2 right-0 flex items-center gap-2 z-60">
<div class="text-xs font-mono text-gray-500 dark:text-gray-400 mr-2">
{{ activeIndex + 1 }} / {{ items.length }}
</div>
<button
- class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-sm border border-gray-200 dark:border-zinc-700"
+ class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-xs border border-gray-200 dark:border-zinc-700"
title="Previous"
@click.stop="prev"
>
<MaterialDesignIcon icon-name="chevron-left" class="size-5" />
</button>
<button
- class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-sm border border-gray-200 dark:border-zinc-700"
+ class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-xs border border-gray-200 dark:border-zinc-700"
title="Next"
@click.stop="next"
>
@@ -57,7 +57,7 @@
<div v-if="items && items.length > 1" class="mt-4 flex justify-center">
<button
- class="flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-xs font-bold text-gray-700 dark:text-gray-300 transition shadow-sm border border-gray-200 dark:border-zinc-700 uppercase tracking-wider"
+ class="flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-xs font-bold text-gray-700 dark:text-gray-300 transition shadow-xs border border-gray-200 dark:border-zinc-700 uppercase tracking-wider"
@click="isExpanded = !isExpanded"
>
<MaterialDesignIcon :icon-name="isExpanded ? 'collapse-all' : 'expand-all'" class="size-4" />
diff --git a/meshchatx/src/frontend/components/ChangelogModal.vue b/meshchatx/src/frontend/components/ChangelogModal.vue
index e865e1ee..dba0acd5 100644
--- a/meshchatx/src/frontend/components/ChangelogModal.vue
+++ b/meshchatx/src/frontend/components/ChangelogModal.vue
@@ -12,7 +12,7 @@
@update:model-value="onVisibleUpdate"
>
<v-card
- class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-900 border-0 overflow-hidden h-full max-h-[100dvh]"
+ class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-900 border-0 overflow-hidden h-full max-h-dvh"
>
<!-- Header -->
<v-toolbar flat color="transparent" class="px-3 sm:px-4 border-b dark:border-zinc-800 shrink-0">
@@ -25,7 +25,7 @@
</v-toolbar-title>
<span
v-if="version"
- class="ml-3 font-black text-[10px] px-2 h-5 tracking-tighter uppercase rounded-sm bg-blue-600 text-white inline-flex items-center"
+ class="ml-3 font-black text-[10px] px-2 h-5 tracking-tighter uppercase rounded-xs bg-blue-600 text-white inline-flex items-center"
>
v{{ version }}
</span>
@@ -50,7 +50,7 @@
<div v-else-if="error" class="flex flex-col items-center justify-center h-full text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
- <button type="button" class="primary-chip !px-6" @click="fetchChangelog">Retry</button>
+ <button type="button" class="primary-chip px-6!" @click="fetchChangelog">Retry</button>
</div>
<div
@@ -86,7 +86,7 @@
></v-checkbox>
</div>
<v-spacer></v-spacer>
- <button type="button" class="primary-chip !px-8 !h-10 !rounded-xl" @click="close">
+ <button type="button" class="primary-chip px-8! h-10! rounded-xl!" @click="close">
{{ $t("common.close", "Close") }}
</button>
</v-card-actions>
@@ -106,7 +106,7 @@
</h1>
<div class="flex items-center gap-2">
<span
- class="font-black text-[10px] px-2 h-5 rounded-sm bg-blue-600 text-white inline-flex items-center"
+ class="font-black text-[10px] px-2 h-5 rounded-xs bg-blue-600 text-white inline-flex items-center"
>
v{{ version }}
</span>
@@ -122,7 +122,7 @@
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
- <button type="button" class="primary-chip !px-6" @click="fetchChangelog">Retry</button>
+ <button type="button" class="primary-chip px-6!" @click="fetchChangelog">Retry</button>
</div>
<div v-else class="changelog-content max-w-none prose dark:prose-invert pb-20">
@@ -250,6 +250,7 @@ export default {
</script>
<style>
+@reference "../style.css";
.changelog-dialog .v-overlay__content {
border-radius: 0.5rem !important;
overflow: hidden;
@@ -265,54 +266,54 @@ export default {
}
.changelog-content {
- @apply leading-relaxed !important;
+ @apply leading-relaxed;
}
.changelog-content h1 {
- @apply text-3xl font-black mt-2 mb-6 text-gray-900 dark:text-white tracking-tight uppercase border-b-2 border-gray-100 dark:border-zinc-800 pb-2 !important;
+ @apply text-3xl font-black mt-2 mb-6 text-gray-900 dark:text-white tracking-tight uppercase border-b-2 border-gray-100 dark:border-zinc-800 pb-2;
}
.changelog-content h2 {
- @apply flex items-center gap-3 text-xl font-bold mt-8 mb-4 text-gray-900 dark:text-white !important;
+ @apply flex items-center gap-3 text-xl font-bold mt-8 mb-4 text-gray-900 dark:text-white;
}
/* Style for [v4.0.0] style headers in markdown */
.changelog-content h2::before {
content: "VERSION";
- @apply text-[10px] font-black bg-blue-500 text-white px-1.5 py-0.5 rounded-sm tracking-tighter !important;
+ @apply text-[10px] font-black bg-blue-500 text-white px-1.5 py-0.5 rounded-xs tracking-tighter;
}
.changelog-content h3 {
- @apply text-lg font-bold mt-6 mb-3 text-blue-600 dark:text-blue-400 flex items-center gap-2 !important;
+ @apply text-lg font-bold mt-6 mb-3 text-blue-600 dark:text-blue-400 flex items-center gap-2;
}
.changelog-content h3::before {
content: "•";
- @apply text-blue-500 font-black !important;
+ @apply text-blue-500 font-black;
}
.changelog-content p {
- @apply my-4 text-gray-700 dark:text-zinc-300 leading-relaxed !important;
+ @apply my-4 text-gray-700 dark:text-zinc-300 leading-relaxed;
}
.changelog-content ul {
- @apply my-6 space-y-3 list-disc pl-6 !important;
+ @apply my-6 space-y-3 list-disc pl-6;
}
.changelog-content li {
- @apply text-gray-600 dark:text-zinc-400 transition-colors hover:text-gray-900 dark:hover:text-white !important;
+ @apply text-gray-600 dark:text-zinc-400 transition-colors hover:text-gray-900 dark:hover:text-white;
}
.changelog-content strong {
- @apply font-bold text-gray-900 dark:text-zinc-100 !important;
+ @apply font-bold text-gray-900 dark:text-zinc-100;
}
.changelog-content code {
- @apply bg-blue-50 dark:bg-blue-900/20 px-1.5 py-0.5 rounded-sm text-blue-700 dark:text-blue-300 font-mono text-[0.85em] border border-blue-100 dark:border-blue-800/30 !important;
+ @apply bg-blue-50 dark:bg-blue-900/20 px-1.5 py-0.5 rounded-xs text-blue-700 dark:text-blue-300 font-mono text-[0.85em] border border-blue-100 dark:border-blue-800/30;
}
.changelog-content hr {
- @apply my-10 border-gray-100 dark:border-zinc-800 !important;
+ @apply my-10 border-gray-100 dark:border-zinc-800;
}
/* Highlight tags like [4.0.0] if they are inside the text */
@@ -321,10 +322,10 @@ export default {
}
.changelog-content h2 {
- @apply py-2 px-4 bg-gray-50 dark:bg-zinc-800/50 rounded-md border border-gray-100 dark:border-zinc-800 !important;
+ @apply py-2 px-4 bg-gray-50 dark:bg-zinc-800/50 rounded-md border border-gray-100 dark:border-zinc-800;
}
.changelog-content .version-tag {
- @apply bg-blue-600 text-white px-2 py-0.5 rounded-sm font-black text-sm tracking-tighter !important;
+ @apply bg-blue-600 text-white px-2 py-0.5 rounded-xs font-black text-sm tracking-tighter;
}
</style>
diff --git a/meshchatx/src/frontend/components/ColourPickerDropdown.vue b/meshchatx/src/frontend/components/ColourPickerDropdown.vue
index 17d469a7..47bde762 100644
--- a/meshchatx/src/frontend/components/ColourPickerDropdown.vue
+++ b/meshchatx/src/frontend/components/ColourPickerDropdown.vue
@@ -9,7 +9,7 @@
<div ref="dropdown-button" @click.stop="toggleMenu">
<slot>
<div
- class="size-8 border border-gray-300 dark:border-zinc-700 rounded shadow cursor-pointer"
+ class="size-8 border border-gray-300 dark:border-zinc-700 rounded-sm shadow-sm cursor-pointer"
:style="{ 'background-color': colour }"
></div>
</slot>
@@ -24,7 +24,7 @@
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
- <div v-if="isShowingMenu" class="absolute left-0 z-[100] mt-2">
+ <div v-if="isShowingMenu" class="absolute left-0 z-100 mt-2">
<v-color-picker
v-model="colourPickerValue"
:modes="['hex']"
diff --git a/meshchatx/src/frontend/components/CommandPalette.vue b/meshchatx/src/frontend/components/CommandPalette.vue
index 259cd688..5cf34db1 100644
--- a/meshchatx/src/frontend/components/CommandPalette.vue
+++ b/meshchatx/src/frontend/components/CommandPalette.vue
@@ -2,10 +2,7 @@
<template>
<transition name="slide-down">
- <div
- v-if="isOpen"
- class="fixed inset-x-0 top-0 z-[200] flex items-start justify-center p-4 pointer-events-none"
- >
+ <div v-if="isOpen" class="fixed inset-x-0 top-0 z-200 flex items-start justify-center p-4 pointer-events-none">
<div
v-click-outside="close"
class="w-full max-w-2xl bg-white/95 dark:bg-zinc-900/95 backdrop-blur-md rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden flex flex-col max-h-[70vh] pointer-events-auto mt-2 sm:mt-8"
@@ -26,7 +23,7 @@
/>
<div class="flex items-center gap-1 ml-2">
<kbd
- class="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg shadow-sm"
+ class="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xs"
>ESC</kbd
>
</div>
@@ -93,14 +90,14 @@
>
<div class="flex items-center gap-1.5">
<kbd
- class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded shadow-sm"
+ class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-sm shadow-xs"
>↑↓</kbd
>
<span>{{ $t("command_palette.footer_navigate") }}</span>
</div>
<div class="flex items-center gap-1.5">
<kbd
- class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded shadow-sm"
+ class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-sm shadow-xs"
>Enter</kbd
>
<span>{{ $t("command_palette.footer_select") }}</span>
diff --git a/meshchatx/src/frontend/components/ConfirmDialog.vue b/meshchatx/src/frontend/components/ConfirmDialog.vue
index e48e10e2..abf79d2a 100644
--- a/meshchatx/src/frontend/components/ConfirmDialog.vue
+++ b/meshchatx/src/frontend/components/ConfirmDialog.vue
@@ -2,8 +2,8 @@
<template>
<Transition name="confirm-dialog">
- <div v-if="pendingConfirm" class="fixed inset-0 z-[9999] flex items-center justify-center p-4">
- <div class="fixed inset-0 bg-black/50 backdrop-blur-sm shadow-2xl" @click="cancel"></div>
+ <div v-if="pendingConfirm" class="fixed inset-0 z-9999 flex items-center justify-center p-4">
+ <div class="fixed inset-0 bg-black/50 backdrop-blur-xs shadow-2xl" @click="cancel"></div>
<div
class="relative w-full sm:w-auto sm:min-w-[400px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-3xl rounded-3xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all"
@@ -12,7 +12,7 @@
<div class="p-8">
<div class="flex items-start mb-6">
<div
- class="flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"
+ class="shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"
>
<MaterialDesignIcon icon-name="alert-circle" class="w-6 h-6" />
</div>
diff --git a/meshchatx/src/frontend/components/DropDownMenu.vue b/meshchatx/src/frontend/components/DropDownMenu.vue
index 35b96860..b0ffb0db 100644
--- a/meshchatx/src/frontend/components/DropDownMenu.vue
+++ b/meshchatx/src/frontend/components/DropDownMenu.vue
@@ -22,7 +22,7 @@
<div
v-if="isShowingMenu && dropdownPosition"
ref="dropdownPanel"
- class="overflow-x-hidden fixed z-[200] w-56 rounded-md bg-white dark:bg-zinc-800 shadow-lg border border-gray-200 dark:border-zinc-700 focus:outline-none"
+ class="overflow-x-hidden fixed z-200 w-56 rounded-md bg-white dark:bg-zinc-800 shadow-lg border border-gray-200 dark:border-zinc-700 focus:outline-hidden"
:style="dropdownPanelStyle"
@click.stop="hideMenu"
>
diff --git a/meshchatx/src/frontend/components/IconButton.vue b/meshchatx/src/frontend/components/IconButton.vue
index 70016a94..9f16052b 100644
--- a/meshchatx/src/frontend/components/IconButton.vue
+++ b/meshchatx/src/frontend/components/IconButton.vue
@@ -3,7 +3,7 @@
<template>
<button
type="button"
- class="text-gray-500 hover:text-gray-700 dark:text-zinc-400 dark:hover:text-zinc-100 hover:bg-gray-100 dark:hover:bg-zinc-800 p-2 rounded-full w-9 h-9 flex items-center justify-center flex-shrink-0 transition-all duration-200"
+ class="text-gray-500 hover:text-gray-700 dark:text-zinc-400 dark:hover:text-zinc-100 hover:bg-gray-100 dark:hover:bg-zinc-800 p-2 rounded-full w-9 h-9 flex items-center justify-center shrink-0 transition-all duration-200"
>
<slot />
</button>
diff --git a/meshchatx/src/frontend/components/LanguageSelector.vue b/meshchatx/src/frontend/components/LanguageSelector.vue
index a2e72fff..7e2fe564 100644
--- a/meshchatx/src/frontend/components/LanguageSelector.vue
+++ b/meshchatx/src/frontend/components/LanguageSelector.vue
@@ -16,7 +16,7 @@
v-if="isDropdownOpen"
ref="languageDropdown"
v-click-outside="closeDropdown"
- class="fixed w-48 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-[9999] overflow-x-hidden"
+ class="fixed w-48 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-9999 overflow-x-hidden"
:style="dropdownStyle"
>
<div class="p-2">
diff --git a/meshchatx/src/frontend/components/NotificationBell.vue b/meshchatx/src/frontend/components/NotificationBell.vue
index 6ce111c2..7bc35275 100644
--- a/meshchatx/src/frontend/components/NotificationBell.vue
+++ b/meshchatx/src/frontend/components/NotificationBell.vue
@@ -21,7 +21,7 @@
v-if="isDropdownOpen"
ref="notificationDropdown"
v-click-outside="closeDropdown"
- class="fixed w-80 sm:w-96 md:max-lg:w-80 lg:w-96 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-[9999] max-h-[min(500px,calc(100vh-2rem))] overflow-hidden flex flex-col"
+ class="fixed w-80 sm:w-96 md:max-lg:w-80 lg:w-96 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-9999 max-h-[min(500px,calc(100vh-2rem))] overflow-hidden flex flex-col"
:style="dropdownStyle"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
@@ -88,7 +88,7 @@
@click="onNotificationClick(notification)"
>
<div class="flex gap-3">
- <div class="flex-shrink-0">
+ <div class="shrink-0">
<div
v-if="notification.lxmf_user_icon"
class="p-2 rounded-lg"
@@ -118,7 +118,7 @@
{{ notification.custom_display_name ?? notification.display_name }}
</div>
<div
- class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap flex-shrink-0"
+ class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap shrink-0"
>
{{ formatTimeAgo(notification.updated_at) }}
</div>
diff --git a/meshchatx/src/frontend/components/SidebarLink.vue b/meshchatx/src/frontend/components/SidebarLink.vue
index 62f545ef..81e93bd7 100644
--- a/meshchatx/src/frontend/components/SidebarLink.vue
+++ b/meshchatx/src/frontend/components/SidebarLink.vue
@@ -10,7 +10,7 @@
? 'bg-blue-100 text-blue-800 group:text-blue-800 dark:bg-zinc-800 dark:text-blue-300'
: 'hover:bg-gray-100 dark:hover:bg-zinc-700',
]"
- class="w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500 overflow-hidden"
+ class="w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500 overflow-hidden"
@click="handleNavigate($event, navigate)"
>
<span class="my-auto shrink-0">
diff --git a/meshchatx/src/frontend/components/Toast.vue b/meshchatx/src/frontend/components/Toast.vue
index 48bb1328..4d071e79 100644
--- a/meshchatx/src/frontend/components/Toast.vue
+++ b/meshchatx/src/frontend/components/Toast.vue
@@ -2,7 +2,7 @@
<template>
<div
- class="fixed bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-[100] flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
+ class="fixed bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
>
<TransitionGroup name="toast">
<div
@@ -12,7 +12,7 @@
:class="toastClass(toast.type)"
>
<!-- icon -->
- <div class="mr-3 flex-shrink-0">
+ <div class="mr-3 shrink-0">
<MaterialDesignIcon
v-if="toast.type === 'success'"
icon-name="check-circle"
diff --git a/meshchatx/src/frontend/components/TutorialModal.vue b/meshchatx/src/frontend/components/TutorialModal.vue
index 15961f9a..33e5749d 100644
--- a/meshchatx/src/frontend/components/TutorialModal.vue
+++ b/meshchatx/src/frontend/components/TutorialModal.vue
@@ -13,7 +13,7 @@
@update:model-value="onVisibleUpdate"
>
<v-card
- class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950 border-0 overflow-hidden relative h-full max-h-[100dvh]"
+ class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950 border-0 overflow-hidden relative h-full max-h-dvh"
>
<!-- Settings Controls -->
<div class="absolute top-4 left-4 z-50 flex items-center gap-1">
@@ -39,7 +39,7 @@
class="h-full transition-all duration-500 ease-out"
:class="[
currentStep >= step ? 'bg-blue-500' : 'bg-transparent',
- currentStep === step ? 'flex-[2]' : 'flex-1',
+ currentStep === step ? 'flex-2' : 'flex-1',
]"
:style="{ borderRight: step < totalSteps ? '1px solid rgba(0,0,0,0.05)' : 'none' }"
></div>
@@ -282,103 +282,218 @@
</p>
</div>
+ <div
+ class="flex items-start gap-3 sm:gap-4 rounded-2xl border border-gray-200 dark:border-zinc-700 bg-white/80 dark:bg-zinc-900/60 p-3.5 sm:p-4"
+ >
+ <div class="shrink-0 pr-0.5 pt-0.5 sm:pt-1 sm:pr-1 flex items-start">
+ <Toggle
+ v-model="defaultBootstrapOnly"
+ @update:model-value="persistDefaultBootstrapOnly"
+ />
+ </div>
+ <div class="min-w-0 flex-1 pl-0.5 sm:pl-0 sm:pt-0.5">
+ <div class="text-sm font-semibold text-gray-900 dark:text-white leading-snug">
+ {{ $t("tutorial.bootstrap_only_label") }}
+ </div>
+ <p class="text-xs text-gray-500 dark:text-zinc-400 mt-1.5 leading-relaxed">
+ {{ $t("tutorial.bootstrap_only_hint") }}
+ </p>
+ </div>
+ </div>
+
<div class="space-y-4">
+ <div
+ v-if="hasAnyBootstrapsToShow"
+ class="w-full max-w-6xl mx-auto flex items-center gap-2 border-0 border-b border-gray-200/90 dark:border-zinc-600/90 py-1.5"
+ >
+ <v-icon icon="mdi-magnify" size="20" class="shrink-0 text-gray-400" />
+ <input
+ v-model="bootstrapListSearch"
+ type="search"
+ autocomplete="off"
+ :placeholder="$t('tutorial.bootstrap_search_placeholder')"
+ class="min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-gray-900 shadow-none ring-0 outline-hidden focus:ring-0 dark:text-zinc-100 placeholder:text-gray-400 dark:placeholder:text-zinc-500"
+ />
+ <button
+ v-if="bootstrapListSearch"
+ type="button"
+ class="shrink-0 rounded p-1 text-gray-400 transition-colors hover:text-gray-700 dark:hover:text-zinc-200"
+ :title="$t('tutorial.bootstrap_search_clear')"
+ :aria-label="$t('tutorial.bootstrap_search_clear')"
+ @click="bootstrapListSearch = ''"
+ >
+ <v-icon icon="mdi-close" size="18" />
+ </button>
+ </div>
+
<div
v-if="sortedDiscoveredInterfaces.length > 0"
- class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl p-4 border border-emerald-500/20"
+ class="h-fit min-w-0 bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl border border-emerald-500/20"
>
- <div class="flex items-center gap-2 mb-3 px-1 text-sm">
- <v-icon icon="mdi-radar" color="emerald"></v-icon>
- <span class="font-bold text-gray-900 dark:text-white">{{
- $t("tutorial.bootstrap_discovered")
- }}</span>
- </div>
- <div class="space-y-2 max-h-[260px] overflow-y-auto pr-2 custom-scrollbar">
- <label
- v-for="iface in sortedDiscoveredInterfaces"
- :key="iface.discovery_hash || iface.name"
- class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
- :class="[
- isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
- ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
- : 'border-gray-100 dark:border-zinc-700 hover:border-emerald-400',
- ]"
- >
- <input
- type="checkbox"
- class="w-4 h-4 accent-emerald-500"
- :checked="isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)"
- @change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
- />
+ <button
+ type="button"
+ class="flex w-full items-center justify-between gap-2 p-4 text-left sm:px-4"
+ :aria-expanded="bootstrapDiscoveredSectionOpen"
+ @click="bootstrapDiscoveredSectionOpen = !bootstrapDiscoveredSectionOpen"
+ >
+ <div class="flex min-w-0 items-center gap-2 text-sm">
<MaterialDesignIcon
- :icon-name="getDiscoveryIcon(iface)"
- class="w-5 h-5 text-emerald-500 shrink-0"
+ :icon-name="bootstrapDiscoveredSectionOpen ? 'chevron-up' : 'chevron-down'"
+ class="size-4 shrink-0 text-gray-500"
/>
- <div class="flex-1 min-w-0">
- <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
- {{ iface.name }}
- </div>
- <div
- class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
- >
- <span v-if="iface.reachable_on"
- >{{ iface.reachable_on
- }}<span v-if="iface.port">:{{ iface.port }}</span></span
+ <v-icon icon="mdi-radar" color="emerald"></v-icon>
+ <span class="font-bold text-gray-900 dark:text-white">{{
+ $t("tutorial.bootstrap_discovered")
+ }}</span>
+ </div>
+ </button>
+ <div v-show="bootstrapDiscoveredSectionOpen" class="px-4 pb-4">
+ <p
+ v-if="
+ bootstrapListSearch &&
+ sortedDiscoveredInterfaces.length > 0 &&
+ filteredDiscoveredForBootstrap.length === 0
+ "
+ class="text-xs text-gray-500 dark:text-zinc-400"
+ >
+ {{ $t("tutorial.bootstrap_search_no_match") }}
+ </p>
+ <div
+ v-else
+ class="space-y-2 max-h-[260px] overflow-y-auto pr-2 pt-1 custom-scrollbar"
+ >
+ <label
+ v-for="iface in filteredDiscoveredForBootstrap"
+ :key="iface.discovery_hash || iface.name"
+ class="flex cursor-pointer items-center gap-3 rounded-xl border bg-white p-3 transition-all dark:bg-zinc-800"
+ :class="[
+ isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
+ ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
+ : 'border-gray-100 dark:border-zinc-700 hover:border-emerald-400',
+ ]"
+ >
+ <input
+ type="checkbox"
+ class="h-4 w-4 accent-emerald-500"
+ :checked="
+ isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
+ "
+ @change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
+ />
+ <MaterialDesignIcon
+ :icon-name="getDiscoveryIcon(iface)"
+ class="h-5 w-5 shrink-0 text-emerald-500"
+ />
+ <div class="min-w-0 flex-1">
+ <div class="truncate text-sm font-bold text-gray-900 dark:text-white">
+ {{ iface.name }}
+ </div>
+ <div
+ class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
>
- <span v-else>{{ iface.type }}</span>
- <span class="ml-2 capitalize">{{ iface.status }}</span>
+ <span v-if="iface.reachable_on"
+ >{{ iface.reachable_on
+ }}<span v-if="iface.port">:{{ iface.port }}</span></span
+ >
+ <span v-else>{{ iface.type }}</span>
+ <span class="ml-2 capitalize">{{ iface.status }}</span>
+ </div>
</div>
- </div>
- </label>
+ </label>
+ </div>
</div>
</div>
<div
- class="bg-gray-50 dark:bg-zinc-900 rounded-3xl p-4 border border-gray-100 dark:border-zinc-800"
+ class="h-fit min-w-0 rounded-3xl border border-gray-100 bg-gray-50 p-0 dark:border-zinc-800 dark:bg-zinc-900"
>
- <div class="flex items-center gap-2 mb-3 px-1 text-sm">
- <v-icon icon="mdi-web" color="blue"></v-icon>
- <span class="font-bold text-gray-900 dark:text-white">{{
- $t("tutorial.bootstrap_community")
- }}</span>
- </div>
- <div class="space-y-2 max-h-[260px] overflow-y-auto pr-2 custom-scrollbar">
- <label
- v-for="iface in communityInterfaces"
- :key="iface.name"
- class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
- :class="[
- isBootstrapSelected(`comm:${iface.name}`)
- ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
- : 'border-gray-100 dark:border-zinc-700 hover:border-blue-400',
- ]"
+ <div class="flex items-center justify-between gap-2 p-4 pr-2 sm:px-4">
+ <button
+ type="button"
+ class="flex min-w-0 flex-1 items-center gap-2 text-left text-sm"
+ :aria-expanded="bootstrapCommunitySectionOpen"
+ @click="bootstrapCommunitySectionOpen = !bootstrapCommunitySectionOpen"
>
- <input
- type="checkbox"
- class="w-4 h-4 accent-blue-500"
- :checked="isBootstrapSelected(`comm:${iface.name}`)"
- @change="toggleBootstrap(`comm:${iface.name}`)"
+ <MaterialDesignIcon
+ :icon-name="bootstrapCommunitySectionOpen ? 'chevron-up' : 'chevron-down'"
+ class="size-4 shrink-0 text-gray-500"
+ />
+ <v-icon icon="mdi-web" color="blue"></v-icon>
+ <span class="font-bold text-gray-900 dark:text-white">{{
+ $t("tutorial.bootstrap_community")
+ }}</span>
+ </button>
+ <button
+ type="button"
+ class="shrink-0 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-blue-600 disabled:opacity-50 dark:hover:bg-zinc-800 dark:hover:text-blue-400"
+ :disabled="refreshingCommunityPresets"
+ :title="$t('interfaces.community_presets_refresh')"
+ :aria-label="$t('interfaces.community_presets_refresh')"
+ @click.stop="refreshCommunityPresets"
+ >
+ <v-icon
+ icon="mdi-refresh"
+ size="20"
+ :class="{ 'animate-spin': refreshingCommunityPresets }"
/>
- <v-icon icon="mdi-server-network" color="blue" size="20"></v-icon>
- <div class="flex-1 min-w-0">
- <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
- {{ iface.name }}
+ </button>
+ </div>
+ <div v-show="bootstrapCommunitySectionOpen" class="px-4 pb-4">
+ <p
+ v-if="
+ bootstrapListSearch &&
+ communityInterfaces.length > 0 &&
+ filteredCommunityForBootstrap.length === 0
+ "
+ class="text-xs text-gray-500 dark:text-zinc-400"
+ >
+ {{ $t("tutorial.bootstrap_search_no_match") }}
+ </p>
+ <div
+ v-else
+ class="space-y-2 max-h-[260px] overflow-y-auto pr-2 pt-1 custom-scrollbar"
+ >
+ <label
+ v-for="iface in filteredCommunityForBootstrap"
+ :key="iface.name"
+ class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 transition-all dark:border-zinc-700 dark:bg-zinc-800"
+ :class="[
+ isBootstrapSelected(`comm:${iface.name}`)
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
+ : 'hover:border-blue-400',
+ ]"
+ >
+ <input
+ type="checkbox"
+ class="h-4 w-4 accent-blue-500"
+ :checked="isBootstrapSelected(`comm:${iface.name}`)"
+ @change="toggleBootstrap(`comm:${iface.name}`)"
+ />
+ <v-icon icon="mdi-server-network" color="blue" size="20"></v-icon>
+ <div class="min-w-0 flex-1">
+ <div class="truncate text-sm font-bold text-gray-900 dark:text-white">
+ {{ iface.name }}
+ </div>
+ <div
+ class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
+ >
+ {{ iface.target_host
+ }}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
+ </div>
</div>
- <div
- class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
+ <span
+ v-if="iface.online"
+ class="shrink-0 text-[9px] font-bold uppercase tracking-widest text-green-500"
+ >{{ $t("tutorial.online") }}</span
>
- {{ iface.target_host
- }}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
- </div>
+ </label>
+ <div v-if="loadingInterfaces" class="flex justify-center py-3">
+ <v-progress-circular
+ indeterminate
+ color="blue"
+ size="24"
+ ></v-progress-circular>
</div>
- <span
- v-if="iface.online"
- class="text-[9px] font-bold text-green-500 uppercase tracking-widest shrink-0"
- >{{ $t("tutorial.online") }}</span
- >
- </label>
- <div v-if="loadingInterfaces" class="flex justify-center py-3">
- <v-progress-circular indeterminate color="blue" size="24"></v-progress-circular>
</div>
</div>
</div>
@@ -401,7 +516,7 @@
</button>
<button
type="button"
- class="px-5 py-2 text-xs rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold shadow transition-all"
+ class="px-5 py-2 text-xs rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold shadow-sm transition-all"
:disabled="
addingBootstraps || reloadingReticulum || selectedBootstrapCount === 0
"
@@ -434,7 +549,7 @@
<div class="flex flex-col items-center gap-6 py-4">
<div
- class="bg-blue-500/10 dark:bg-blue-500/20 p-6 rounded-[2rem] text-center space-y-4 border border-blue-500/20 max-w-md"
+ class="bg-blue-500/10 dark:bg-blue-500/20 p-6 rounded-4xl text-center space-y-4 border border-blue-500/20 max-w-md"
>
<v-icon icon="mdi-server-network" color="blue" size="48"></v-icon>
<div class="text-lg font-bold text-gray-900 dark:text-white">
@@ -461,7 +576,7 @@
</button>
<button
type="button"
- class="w-full px-6 py-3 rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-sm transition-all transform hover:scale-[1.02]"
+ class="w-full px-6 py-3 rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-xs transition-all transform hover:scale-[1.02]"
@click="nextStep"
>
{{ $t("tutorial.propagation_skip_auto") }}
@@ -506,14 +621,14 @@
<a
href="/meshchatx-docs/index.html"
target="_blank"
- class="px-3 py-1 text-[10px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all inline-block"
+ class="px-3 py-1 text-[10px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-xs transition-all inline-block"
>
{{ $t("tutorial.meshchatx_docs") }}
</a>
<a
:href="reticulumBundledDocsUrl"
target="_blank"
- class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-block"
+ class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-block"
>
{{ $t("tutorial.reticulum_docs") }}
</a>
@@ -534,7 +649,7 @@
</div>
<button
type="button"
- class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
+ class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="gotoRoute('micron-editor')"
>
{{ $t("tutorial.open_micron_editor") }}
@@ -643,7 +758,7 @@
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
- class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
+ class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="previousStep"
>
{{ $t("tutorial.back") }}
@@ -654,7 +769,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
- class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
+ class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
@click="skipTutorial"
>
{{ $t("tutorial.skip") }}
@@ -663,7 +778,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
- class="px-8 h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold text-sm shadow-sm transition-all"
+ class="px-8 h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold text-sm shadow-xs transition-all"
@click="nextStep"
>
{{ $t("tutorial.next") }}
@@ -672,7 +787,7 @@
<button
v-else
type="button"
- class="px-8 h-12 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm shadow-sm transition-all"
+ class="px-8 h-12 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm shadow-xs transition-all"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
@@ -707,7 +822,7 @@
class="h-full transition-all duration-500 ease-out"
:class="[
currentStep >= step ? 'bg-blue-500' : 'bg-transparent',
- currentStep === step ? 'flex-[2]' : 'flex-1',
+ currentStep === step ? 'flex-2' : 'flex-1',
]"
:style="{ borderRight: step < totalSteps ? '1px solid rgba(0,0,0,0.05)' : 'none' }"
></div>
@@ -950,104 +1065,223 @@
</p>
</div>
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl mx-auto">
+ <div
+ class="flex items-start gap-3 sm:gap-5 max-w-3xl mx-auto rounded-2xl border border-gray-200 dark:border-zinc-700 bg-white/80 dark:bg-zinc-900/60 p-3.5 sm:p-5"
+ >
+ <div class="shrink-0 pr-0.5 pt-0.5 sm:pt-1.5 sm:pr-1 flex items-start">
+ <Toggle
+ v-model="defaultBootstrapOnly"
+ @update:model-value="persistDefaultBootstrapOnly"
+ />
+ </div>
+ <div class="min-w-0 flex-1 pl-0.5 sm:pl-0 sm:pt-0.5">
+ <div
+ class="text-sm sm:text-base font-semibold text-gray-900 dark:text-white leading-snug"
+ >
+ {{ $t("tutorial.bootstrap_only_label") }}
+ </div>
+ <p
+ class="text-xs sm:text-sm text-gray-500 dark:text-zinc-400 mt-1.5 sm:mt-2 leading-relaxed"
+ >
+ {{ $t("tutorial.bootstrap_only_hint") }}
+ </p>
+ </div>
+ </div>
+
+ <div
+ v-if="hasAnyBootstrapsToShow"
+ class="flex w-full max-w-6xl mx-auto items-center gap-2 border-0 border-b border-gray-200/90 dark:border-zinc-600/90 py-1.5"
+ >
+ <v-icon icon="mdi-magnify" size="22" class="shrink-0 text-gray-400" />
+ <input
+ v-model="bootstrapListSearch"
+ type="search"
+ autocomplete="off"
+ :placeholder="$t('tutorial.bootstrap_search_placeholder')"
+ class="min-w-0 flex-1 border-0 bg-transparent p-0 text-base text-gray-900 shadow-none ring-0 outline-hidden focus:ring-0 dark:text-zinc-100 placeholder:text-gray-400 dark:placeholder:text-zinc-500"
+ />
+ <button
+ v-if="bootstrapListSearch"
+ type="button"
+ class="shrink-0 rounded p-1.5 text-gray-400 transition-colors hover:text-gray-700 dark:hover:text-zinc-200"
+ :title="$t('tutorial.bootstrap_search_clear')"
+ :aria-label="$t('tutorial.bootstrap_search_clear')"
+ @click="bootstrapListSearch = ''"
+ >
+ <v-icon icon="mdi-close" size="20" />
+ </button>
+ </div>
+
+ <div class="grid max-w-6xl mx-auto grid-cols-1 items-start gap-6 lg:grid-cols-2">
<div
v-if="sortedDiscoveredInterfaces.length > 0"
- class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-[1.5rem] p-5 border border-emerald-500/20"
+ class="h-fit min-w-0 bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl border border-emerald-500/20"
>
- <div class="flex items-center gap-2 mb-4">
- <v-icon icon="mdi-radar" color="emerald" size="22"></v-icon>
- <span class="text-base font-bold text-gray-900 dark:text-white">{{
- $t("tutorial.bootstrap_discovered")
- }}</span>
- </div>
- <div class="space-y-2 max-h-[480px] overflow-y-auto pr-2 custom-scrollbar">
- <label
- v-for="iface in sortedDiscoveredInterfaces"
- :key="iface.discovery_hash || iface.name"
- class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
- :class="[
- isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
- ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
- : 'border-gray-100 dark:border-zinc-700 hover:border-emerald-400',
- ]"
- >
- <input
- type="checkbox"
- class="w-4 h-4 accent-emerald-500"
- :checked="isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)"
- @change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
- />
+ <button
+ type="button"
+ class="flex w-full items-center justify-between gap-2 p-4 text-left sm:px-5"
+ :aria-expanded="bootstrapDiscoveredSectionOpen"
+ @click="bootstrapDiscoveredSectionOpen = !bootstrapDiscoveredSectionOpen"
+ >
+ <div class="flex min-w-0 items-center gap-2.5 text-base">
<MaterialDesignIcon
- :icon-name="getDiscoveryIcon(iface)"
- class="w-5 h-5 text-emerald-500 shrink-0"
+ :icon-name="bootstrapDiscoveredSectionOpen ? 'chevron-up' : 'chevron-down'"
+ class="size-4 shrink-0 text-gray-500"
/>
- <div class="flex-1 min-w-0">
- <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
- {{ iface.name }}
- </div>
- <div
- class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
- >
- <span v-if="iface.reachable_on"
- >{{ iface.reachable_on
- }}<span v-if="iface.port">:{{ iface.port }}</span></span
+ <v-icon icon="mdi-radar" color="emerald" size="22"></v-icon>
+ <span class="font-bold text-gray-900 dark:text-white">{{
+ $t("tutorial.bootstrap_discovered")
+ }}</span>
+ </div>
+ </button>
+ <div v-show="bootstrapDiscoveredSectionOpen" class="px-4 pb-5 sm:px-5">
+ <p
+ v-if="
+ bootstrapListSearch &&
+ sortedDiscoveredInterfaces.length > 0 &&
+ filteredDiscoveredForBootstrap.length === 0
+ "
+ class="text-sm text-gray-500 dark:text-zinc-400"
+ >
+ {{ $t("tutorial.bootstrap_search_no_match") }}
+ </p>
+ <div
+ v-else
+ class="max-h-[480px] space-y-2 overflow-y-auto pr-2 pt-1 custom-scrollbar"
+ >
+ <label
+ v-for="iface in filteredDiscoveredForBootstrap"
+ :key="iface.discovery_hash || iface.name"
+ class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 transition-all dark:border-zinc-700 dark:bg-zinc-800"
+ :class="[
+ isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
+ ? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
+ : 'hover:border-emerald-400',
+ ]"
+ >
+ <input
+ type="checkbox"
+ class="h-4 w-4 accent-emerald-500"
+ :checked="
+ isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
+ "
+ @change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
+ />
+ <MaterialDesignIcon
+ :icon-name="getDiscoveryIcon(iface)"
+ class="h-5 w-5 shrink-0 text-emerald-500"
+ />
+ <div class="min-w-0 flex-1">
+ <div class="truncate text-sm font-bold text-gray-900 dark:text-white">
+ {{ iface.name }}
+ </div>
+ <div
+ class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
>
- <span v-else>{{ iface.type }}</span>
- <span class="ml-2 capitalize">{{ iface.status }}</span>
+ <span v-if="iface.reachable_on"
+ >{{ iface.reachable_on
+ }}<span v-if="iface.port">:{{ iface.port }}</span></span
+ >
+ <span v-else>{{ iface.type }}</span>
+ <span class="ml-2 capitalize">{{ iface.status }}</span>
+ </div>
</div>
- </div>
- </label>
+ </label>
+ </div>
</div>
</div>
<div
- class="bg-gray-50 dark:bg-zinc-900 rounded-[1.5rem] p-5 border border-gray-100 dark:border-zinc-800"
+ class="h-fit min-w-0 rounded-3xl border border-gray-100 bg-gray-50 p-0 dark:border-zinc-800 dark:bg-zinc-900"
:class="[sortedDiscoveredInterfaces.length === 0 ? 'lg:col-span-2' : '']"
>
- <div class="flex items-center gap-2 mb-4">
- <v-icon icon="mdi-web" color="blue" size="22"></v-icon>
- <span class="text-base font-bold text-gray-900 dark:text-white">{{
- $t("tutorial.bootstrap_community")
- }}</span>
- </div>
- <div class="space-y-2 max-h-[480px] overflow-y-auto pr-2 custom-scrollbar">
- <label
- v-for="iface in communityInterfaces"
- :key="iface.name"
- class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
- :class="[
- isBootstrapSelected(`comm:${iface.name}`)
- ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
- : 'border-gray-100 dark:border-zinc-700 hover:border-blue-400',
- ]"
+ <div class="flex items-center justify-between gap-2 p-4 pr-2 sm:px-5">
+ <button
+ type="button"
+ class="flex min-w-0 flex-1 items-center gap-2.5 text-left text-base"
+ :aria-expanded="bootstrapCommunitySectionOpen"
+ @click="bootstrapCommunitySectionOpen = !bootstrapCommunitySectionOpen"
>
- <input
- type="checkbox"
- class="w-4 h-4 accent-blue-500"
- :checked="isBootstrapSelected(`comm:${iface.name}`)"
- @change="toggleBootstrap(`comm:${iface.name}`)"
+ <MaterialDesignIcon
+ :icon-name="bootstrapCommunitySectionOpen ? 'chevron-up' : 'chevron-down'"
+ class="size-4 shrink-0 text-gray-500"
/>
- <v-icon icon="mdi-server-network" color="blue" size="22"></v-icon>
- <div class="flex-1 min-w-0">
- <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
- {{ iface.name }}
+ <v-icon icon="mdi-web" color="blue" size="22"></v-icon>
+ <span class="font-bold text-gray-900 dark:text-white">{{
+ $t("tutorial.bootstrap_community")
+ }}</span>
+ </button>
+ <button
+ type="button"
+ class="shrink-0 rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-blue-600 disabled:opacity-50 dark:hover:bg-zinc-800 dark:hover:text-blue-400"
+ :disabled="refreshingCommunityPresets"
+ :title="$t('interfaces.community_presets_refresh')"
+ :aria-label="$t('interfaces.community_presets_refresh')"
+ @click.stop="refreshCommunityPresets"
+ >
+ <v-icon
+ icon="mdi-refresh"
+ size="22"
+ :class="{ 'animate-spin': refreshingCommunityPresets }"
+ />
+ </button>
+ </div>
+ <div v-show="bootstrapCommunitySectionOpen" class="px-4 pb-5 sm:px-5">
+ <p
+ v-if="
+ bootstrapListSearch &&
+ communityInterfaces.length > 0 &&
+ filteredCommunityForBootstrap.length === 0
+ "
+ class="text-sm text-gray-500 dark:text-zinc-400"
+ >
+ {{ $t("tutorial.bootstrap_search_no_match") }}
+ </p>
+ <div
+ v-else
+ class="max-h-[480px] space-y-2 overflow-y-auto pr-2 pt-1 custom-scrollbar"
+ >
+ <label
+ v-for="iface in filteredCommunityForBootstrap"
+ :key="iface.name"
+ class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 transition-all dark:border-zinc-700 dark:bg-zinc-800"
+ :class="[
+ isBootstrapSelected(`comm:${iface.name}`)
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
+ : 'hover:border-blue-400',
+ ]"
+ >
+ <input
+ type="checkbox"
+ class="h-4 w-4 accent-blue-500"
+ :checked="isBootstrapSelected(`comm:${iface.name}`)"
+ @change="toggleBootstrap(`comm:${iface.name}`)"
+ />
+ <v-icon icon="mdi-server-network" color="blue" size="22"></v-icon>
+ <div class="min-w-0 flex-1">
+ <div class="truncate text-sm font-bold text-gray-900 dark:text-white">
+ {{ iface.name }}
+ </div>
+ <div
+ class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
+ >
+ {{ iface.target_host
+ }}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
+ </div>
</div>
- <div
- class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
+ <span
+ v-if="iface.online"
+ class="shrink-0 text-[9px] font-bold uppercase tracking-widest text-green-500"
+ >{{ $t("tutorial.online") }}</span
>
- {{ iface.target_host
- }}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
- </div>
+ </label>
+ <div v-if="loadingInterfaces" class="flex justify-center py-3">
+ <v-progress-circular
+ indeterminate
+ color="blue"
+ size="24"
+ ></v-progress-circular>
</div>
- <span
- v-if="iface.online"
- class="text-[9px] font-bold text-green-500 uppercase tracking-widest shrink-0"
- >{{ $t("tutorial.online") }}</span
- >
- </label>
- <div v-if="loadingInterfaces" class="flex justify-center py-3">
- <v-progress-circular indeterminate color="blue" size="24"></v-progress-circular>
</div>
</div>
</div>
@@ -1161,7 +1395,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div
- class="flex flex-col items-center gap-6 p-8 rounded-[2rem] bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
+ class="flex flex-col items-center gap-6 p-8 rounded-4xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-book-open-variant" color="blue" size="64"></v-icon>
<div>
@@ -1175,14 +1409,14 @@
<a
href="/meshchatx-docs/index.html"
target="_blank"
- class="h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all inline-flex items-center justify-center px-6"
+ class="h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-xs transition-all inline-flex items-center justify-center px-6"
>
{{ $t("tutorial.read_meshchatx_docs") }}
</a>
<a
:href="reticulumBundledDocsUrl"
target="_blank"
- class="h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-flex items-center justify-center px-6"
+ class="h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-flex items-center justify-center px-6"
>
{{ $t("tutorial.reticulum_manual") }}
</a>
@@ -1191,7 +1425,7 @@
</div>
<div
- class="flex flex-col items-center gap-6 p-8 rounded-[2rem] bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
+ class="flex flex-col items-center gap-6 p-8 rounded-4xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-file-document-edit-outline" color="orange" size="64"></v-icon>
<div>
@@ -1203,7 +1437,7 @@
</p>
<button
type="button"
- class="w-full h-12 rounded-xl bg-orange-600 hover:bg-orange-500 text-white font-semibold shadow-sm transition-all"
+ class="w-full h-12 rounded-xl bg-orange-600 hover:bg-orange-500 text-white font-semibold shadow-xs transition-all"
@click="gotoRoute('micron-editor')"
>
{{ $t("tutorial.open_micron_editor") }}
@@ -1312,7 +1546,7 @@
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
- class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
+ class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="previousStep"
>
{{ $t("tutorial.back") }}
@@ -1323,7 +1557,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
- class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
+ class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
@click="skipTutorial"
>
{{ $t("tutorial.skip_setup") }}
@@ -1332,7 +1566,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
- class="px-12 h-14 text-lg rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all"
+ class="px-12 h-14 text-lg rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-xs transition-all"
@click="nextStep"
>
{{ $t("tutorial.continue") }}
@@ -1341,7 +1575,7 @@
<button
v-else
type="button"
- class="px-12 h-14 text-lg rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold shadow-sm transition-all"
+ class="px-12 h-14 text-lg rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold shadow-xs transition-all"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
@@ -1361,12 +1595,14 @@ import GlobalState from "../js/GlobalState";
import { bundledReticulumDocsUrl } from "../js/reticulumDocsEntryUrl.js";
import LanguageSelector from "./LanguageSelector.vue";
import MaterialDesignIcon from "./MaterialDesignIcon.vue";
+import Toggle from "./forms/Toggle.vue";
export default {
name: "TutorialModal",
components: {
LanguageSelector,
MaterialDesignIcon,
+ Toggle,
},
data() {
return {
@@ -1391,6 +1627,11 @@ export default {
discoveryInterval: null,
markingSeen: false,
windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
+ defaultBootstrapOnly: true,
+ refreshingCommunityPresets: false,
+ bootstrapListSearch: "",
+ bootstrapDiscoveredSectionOpen: true,
+ bootstrapCommunitySectionOpen: true,
};
},
computed: {
@@ -1418,6 +1659,37 @@ export default {
hasAnyBootstrapsToShow() {
return this.communityInterfaces.length > 0 || this.sortedDiscoveredInterfaces.length > 0;
},
+ filteredDiscoveredForBootstrap() {
+ const list = this.sortedDiscoveredInterfaces;
+ const q = (this.bootstrapListSearch || "").trim().toLowerCase();
+ if (!q) {
+ return list;
+ }
+ return list.filter((iface) => {
+ const parts = [
+ iface.name,
+ iface.type,
+ iface.reachable_on,
+ String(iface.port ?? ""),
+ iface.status,
+ iface.discovery_hash,
+ ].filter(Boolean);
+ return parts.join(" ").toLowerCase().includes(q);
+ });
+ },
+ filteredCommunityForBootstrap() {
+ const list = this.communityInterfaces;
+ const q = (this.bootstrapListSearch || "").trim().toLowerCase();
+ if (!q) {
+ return list;
+ }
+ return list.filter((iface) => {
+ const parts = [iface.name, iface.target_host, String(iface.target_port ?? ""), iface.type].filter(
+ Boolean
+ );
+ return parts.join(" ").toLowerCase().includes(q);
+ });
+ },
selectedBootstrapCount() {
return this.selectedBootstrapKeys.length;
},
@@ -1439,6 +1711,7 @@ export default {
};
window.addEventListener("resize", this.onWindowResize, { passive: true });
if (this.isPage) {
+ this.loadDiscoveryBootstrapDefaults();
this.loadCommunityInterfaces();
this.loadDiscoveredInterfaces();
this.discoveryInterval = setInterval(() => {
@@ -1476,6 +1749,10 @@ export default {
this.connectionMode = null;
this.selectedBootstrapKeys = [];
this.addedBootstrapKeys = [];
+ this.bootstrapListSearch = "";
+ this.bootstrapDiscoveredSectionOpen = true;
+ this.bootstrapCommunitySectionOpen = true;
+ await this.loadDiscoveryBootstrapDefaults();
await this.loadCommunityInterfaces();
await this.loadDiscoveredInterfaces();
@@ -1497,6 +1774,21 @@ export default {
this.loadingInterfaces = false;
}
},
+ async refreshCommunityPresets() {
+ if (this.refreshingCommunityPresets) return;
+ this.refreshingCommunityPresets = true;
+ try {
+ const r = await window.api.post("/api/v1/community-interfaces/refresh", {});
+ const n = r.data?.count ?? 0;
+ ToastUtils.success(this.$t("interfaces.community_presets_refreshed", { count: n }));
+ await this.loadCommunityInterfaces();
+ } catch (e) {
+ ToastUtils.error(e.response?.data?.message || this.$t("interfaces.community_presets_refresh_failed"));
+ console.error(e);
+ } finally {
+ this.refreshingCommunityPresets = false;
+ }
+ },
async loadDiscoveredInterfaces() {
this.loadingDiscovered = true;
try {
@@ -1532,11 +1824,16 @@ export default {
const payload = {
discover_interfaces: true,
autoconnect_discovered_interfaces: 3,
+ default_bootstrap_only: true,
};
await window.api.patch(`/api/v1/reticulum/discovery`, payload);
+ this.defaultBootstrapOnly = true;
ToastUtils.success(this.$t("tutorial.discovery_enabled"));
this.connectionMode = "discovery";
this.currentStep = 3;
+ this.bootstrapListSearch = "";
+ this.bootstrapDiscoveredSectionOpen = true;
+ this.bootstrapCommunitySectionOpen = true;
} catch (e) {
console.error("Failed to enable discovery:", e);
ToastUtils.error(this.$t("tutorial.failed_enable_discovery"));
@@ -1589,6 +1886,7 @@ export default {
name: iface.name || `Discovered ${iface.discovery_hash || ""}`.trim(),
type: iface.type === "BackboneInterface" ? "TCPClientInterface" : iface.type,
enabled: true,
+ bootstrap_only: this.defaultBootstrapOnly === true,
};
if (iface.reachable_on) {
payload.target_host = iface.reachable_on;
@@ -1605,8 +1903,37 @@ export default {
target_host: iface.target_host,
target_port: iface.target_port,
enabled: true,
+ bootstrap_only: this.defaultBootstrapOnly === true,
};
},
+ parseDiscoveryBool(value, defaultValue = true) {
+ if (value === undefined || value === null || value === "") {
+ return defaultValue;
+ }
+ if (typeof value === "string") {
+ return ["true", "yes", "1", "y", "on"].includes(value.toLowerCase());
+ }
+ return Boolean(value);
+ },
+ async loadDiscoveryBootstrapDefaults() {
+ try {
+ const response = await window.api.get("/api/v1/reticulum/discovery");
+ const d = response.data?.discovery ?? {};
+ this.defaultBootstrapOnly = this.parseDiscoveryBool(d.default_bootstrap_only, true);
+ } catch (e) {
+ console.error(e);
+ this.defaultBootstrapOnly = true;
+ }
+ },
+ async persistDefaultBootstrapOnly(value) {
+ try {
+ await window.api.patch("/api/v1/reticulum/discovery", {
+ default_bootstrap_only: value === true,
+ });
+ } catch (e) {
+ console.error("Failed to save default_bootstrap_only:", e);
+ }
+ },
async confirmBootstraps() {
if (this.addingBootstraps) return;
if (this.selectedBootstrapKeys.length === 0) {
@@ -1761,6 +2088,11 @@ export default {
return;
}
this.currentStep++;
+ if (this.currentStep === 3) {
+ this.bootstrapListSearch = "";
+ this.bootstrapDiscoveredSectionOpen = true;
+ this.bootstrapCommunitySectionOpen = true;
+ }
},
previousStep() {
if (this.currentStep <= 1) return;
diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue
index 43f0df74..79e2efca 100644
--- a/meshchatx/src/frontend/components/about/AboutPage.vue
+++ b/meshchatx/src/frontend/components/about/AboutPage.vue
@@ -284,18 +284,18 @@
</div>
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 text-sm min-w-0">
<div>
- <div class="glass-label !text-[10px] mb-2 opacity-50">
+ <div class="glass-label text-[10px]! mb-2 opacity-50">
{{ $t("about.reticulum_config") }}
</div>
<div
- class="monospace-field !bg-zinc-50 dark:!bg-zinc-950 break-all text-[11px] !p-3 rounded-xl border border-zinc-100 dark:border-zinc-800"
+ class="monospace-field bg-zinc-50! dark:bg-zinc-950! break-all text-[11px] p-3! rounded-xl border border-zinc-100 dark:border-zinc-800"
>
{{ appInfo.reticulum_config_path || $t("about.path_unknown") }}
</div>
<button
v-if="isElectron"
type="button"
- class="secondary-chip mt-3 !px-3 !py-1 !text-[10px]"
+ class="secondary-chip mt-3 px-3! py-1! text-[10px]!"
@click="showReticulumConfigFile"
>
<v-icon icon="mdi-folder-open" start size="14"></v-icon>
@@ -303,18 +303,18 @@
</button>
</div>
<div>
- <div class="glass-label !text-[10px] mb-2 opacity-50">
+ <div class="glass-label text-[10px]! mb-2 opacity-50">
{{ $t("about.database_path") }}
</div>
<div
- class="monospace-field !bg-zinc-50 dark:!bg-zinc-950 break-all text-[11px] !p-3 rounded-xl border border-zinc-100 dark:border-zinc-800"
+ class="monospace-field bg-zinc-50! dark:bg-zinc-950! break-all text-[11px] p-3! rounded-xl border border-zinc-100 dark:border-zinc-800"
>
{{ appInfo.database_path || $t("about.path_unknown") }}
</div>
<button
v-if="isElectron"
type="button"
- class="secondary-chip mt-3 !px-3 !py-1 !text-[10px]"
+ class="secondary-chip mt-3 px-3! py-1! text-[10px]!"
@click="showDatabaseFile"
>
<v-icon icon="mdi-database-search" start size="14"></v-icon>
@@ -322,7 +322,7 @@
</button>
</div>
<div
- class="flex flex-col justify-center space-y-3 py-2 sm:py-3 border-t sm:border border-gray-200/60 dark:border-zinc-800/80 sm:rounded-xl sm:p-4 sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
+ class="flex flex-col justify-center space-y-3 py-2 sm:py-3 border-t sm:border border-gray-200/60 dark:border-zinc-800/80 sm:rounded-xl sm:p-4 sm:bg-black/2 dark:sm:bg-white/2"
>
<div
v-if="config"
@@ -411,7 +411,7 @@
<div class="flex flex-col space-y-8 min-w-0">
<div class="flex items-center gap-5">
<div
- class="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 shadow-sm"
+ class="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 shadow-xs"
>
<img
src="../../public/favicons/favicon-512x512.png"
@@ -431,10 +431,10 @@
class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
>
<div
- class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-blue-500 to-emerald-500"
+ class="absolute left-[-2px] top-0 bottom-0 w-[2px] bg-linear-to-b from-blue-500 to-emerald-500"
></div>
<div
- class="w-12 h-12 rounded-2xl bg-emerald-500/10 flex items-center justify-center border border-emerald-500/20 text-emerald-600 font-black text-[10px] tracking-tighter shadow-sm"
+ class="w-12 h-12 rounded-2xl bg-emerald-500/10 flex items-center justify-center border border-emerald-500/20 text-emerald-600 font-black text-[10px] tracking-tighter shadow-xs"
>
LXMFy
</div>
@@ -451,10 +451,10 @@
class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
>
<div
- class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-emerald-500 to-purple-500"
+ class="absolute left-[-2px] top-0 bottom-0 w-[2px] bg-linear-to-b from-emerald-500 to-purple-500"
></div>
<div
- class="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center border border-purple-500/20 text-purple-600 font-black text-[10px] tracking-tighter shadow-sm"
+ class="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center border border-purple-500/20 text-purple-600 font-black text-[10px] tracking-tighter shadow-xs"
>
LXMF
</div>
@@ -471,10 +471,10 @@
class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
>
<div
- class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-purple-500 to-indigo-500"
+ class="absolute left-[-2px] top-0 bottom-0 w-[2px] bg-linear-to-b from-purple-500 to-indigo-500"
></div>
<div
- class="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 text-indigo-600 font-black text-[10px] tracking-tighter shadow-sm"
+ class="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 text-indigo-600 font-black text-[10px] tracking-tighter shadow-xs"
>
RNS
</div>
@@ -492,7 +492,7 @@
? 'bg-blue-500/10 text-blue-500 border-blue-500/20'
: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20',
]"
- class="text-[8px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded border max-w-full break-words"
+ class="text-[8px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded-sm border max-w-full wrap-break-word"
>
{{
appInfo.is_connected_to_shared_instance
@@ -511,7 +511,7 @@
<div class="space-y-8 min-w-0">
<div
- class="py-4 sm:p-5 border-t border-gray-200/60 dark:border-zinc-800/80 sm:border sm:rounded-2xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02] min-w-0"
+ class="py-4 sm:p-5 border-t border-gray-200/60 dark:border-zinc-800/80 sm:border sm:rounded-2xl sm:bg-black/2 dark:sm:bg-white/2 min-w-0"
>
<div
class="text-[10px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-4"
@@ -601,7 +601,7 @@
<div class="flex flex-wrap gap-2 w-full md:w-auto">
<button
type="button"
- class="secondary-chip !px-4 !py-1.5 !text-xs min-h-[44px] sm:min-h-0"
+ class="secondary-chip px-4! py-1.5! text-xs! min-h-[44px] sm:min-h-0"
:disabled="databaseActionInProgress || healthLoading"
@click="getDatabaseHealth(true)"
>
@@ -611,7 +611,7 @@
</button>
<button
type="button"
- class="primary-chip !px-4 !py-1.5 !text-xs"
+ class="primary-chip px-4! py-1.5! text-xs!"
:disabled="databaseActionInProgress"
@click="vacuumDatabase"
>
@@ -619,7 +619,7 @@
</button>
<button
type="button"
- class="danger-chip !px-4 !py-1.5 !text-xs"
+ class="danger-chip px-4! py-1.5! text-xs!"
:disabled="databaseActionInProgress"
@click="runRecovery"
>
@@ -699,7 +699,7 @@
</div>
<button
type="button"
- class="primary-chip !px-5 !py-2.5"
+ class="primary-chip px-5! py-2.5!"
:disabled="backupInProgress"
@click="backupDatabase"
>
@@ -728,11 +728,11 @@
v-model="snapshotName"
type="text"
:placeholder="$t('about.snapshot_placeholder')"
- class="bg-zinc-50 dark:bg-zinc-900 px-4 py-2 rounded-xl text-sm border border-zinc-100 dark:border-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-500/20 flex-1 md:min-w-[200px]"
+ class="bg-zinc-50 dark:bg-zinc-900 px-4 py-2 rounded-xl text-sm border border-zinc-100 dark:border-zinc-800 focus:outline-hidden focus:ring-2 focus:ring-blue-500/20 flex-1 md:min-w-[200px]"
/>
<button
type="button"
- class="primary-chip !px-6"
+ class="primary-chip px-6!"
:disabled="snapshotInProgress"
@click="createSnapshot"
>
@@ -747,7 +747,7 @@
<div
v-for="snapshot in snapshots"
:key="snapshot.path"
- class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/[0.02] dark:sm:bg-white/[0.02] transition-colors"
+ class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/2 dark:sm:bg-white/2 transition-colors"
>
<div class="flex flex-col min-w-0">
<span
@@ -764,7 +764,7 @@
>
<button
type="button"
- class="primary-chip !px-3 !py-1 !text-[10px]"
+ class="primary-chip px-3! py-1! text-[10px]!"
@click="downloadSnapshot(snapshot.name)"
>
<v-icon icon="mdi-download" size="12" start></v-icon>
@@ -772,14 +772,14 @@
</button>
<button
type="button"
- class="secondary-chip !px-3 !py-1 !text-[10px]"
+ class="secondary-chip px-3! py-1! text-[10px]!"
@click="restoreFromSnapshot(snapshot.path)"
>
{{ $t("about.snapshot_restore") }}
</button>
<button
type="button"
- class="danger-chip !px-3 !py-1 !text-[10px]"
+ class="danger-chip px-3! py-1! text-[10px]!"
@click="deleteSnapshot(snapshot.name)"
>
<v-icon icon="mdi-delete" size="12"></v-icon>
@@ -803,14 +803,14 @@
</div>
<div class="flex gap-2">
<button
- class="secondary-chip !p-1 disabled:opacity-30"
+ class="secondary-chip p-1! disabled:opacity-30"
:disabled="snapshotsOffset === 0"
@click="prevSnapshots"
>
<v-icon icon="mdi-chevron-left"></v-icon>
</button>
<button
- class="secondary-chip !p-1 disabled:opacity-30"
+ class="secondary-chip p-1! disabled:opacity-30"
:disabled="snapshotsOffset + snapshotsLimit >= snapshotsTotal"
@click="nextSnapshots"
>
@@ -842,7 +842,7 @@
<div
v-for="backup in autoBackups"
:key="backup.path"
- class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/[0.02] dark:sm:bg-white/[0.02] transition-colors"
+ class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/2 dark:sm:bg-white/2 transition-colors"
>
<div class="flex flex-col min-w-0">
<span
@@ -859,7 +859,7 @@
>
<button
type="button"
- class="primary-chip !px-3 !py-1 !text-[10px]"
+ class="primary-chip px-3! py-1! text-[10px]!"
@click="downloadBackupFile(backup.name)"
>
<v-icon icon="mdi-download" size="12" start></v-icon>
@@ -867,14 +867,14 @@
</button>
<button
type="button"
- class="secondary-chip !px-3 !py-1 !text-[10px]"
+ class="secondary-chip px-3! py-1! text-[10px]!"
@click="restoreFromSnapshot(backup.path)"
>
{{ $t("about.snapshot_restore") }}
</button>
<button
type="button"
- class="danger-chip !px-3 !py-1 !text-[10px]"
+ class="danger-chip px-3! py-1! text-[10px]!"
@click="deleteBackup(backup.name)"
>
<v-icon icon="mdi-delete" size="12"></v-icon>
@@ -898,14 +898,14 @@
</div>
<div class="flex gap-2">
<button
- class="secondary-chip !p-1 disabled:opacity-30"
+ class="secondary-chip p-1! disabled:opacity-30"
:disabled="autoBackupsOffset === 0"
@click="prevBackups"
>
<v-icon icon="mdi-chevron-left"></v-icon>
</button>
<button
- class="secondary-chip !p-1 disabled:opacity-30"
+ class="secondary-chip p-1! disabled:opacity-30"
:disabled="autoBackupsOffset + autoBackupsLimit >= autoBackupsTotal"
@click="nextBackups"
>
@@ -1254,9 +1254,13 @@ export default {
if (response.data.database?.health) {
this.databaseHealth = response.data.database.health;
}
- this.databaseActionMessage = response.data.message || "Database vacuum completed";
+ const msg = response.data.message || this.$t("about.vacuum_complete");
+ this.databaseActionMessage = msg;
+ ToastUtils.success(this.$t("about.vacuum_complete"));
} catch (e) {
- this.databaseActionError = "Vacuum failed";
+ this.databaseActionError = this.$t("about.vacuum_failed");
+ const detail = e?.response?.data?.message;
+ ToastUtils.error(detail || this.$t("about.vacuum_failed"));
console.log(e);
} finally {
this.databaseActionInProgress = false;
@@ -1326,6 +1330,9 @@ export default {
if (this.databaseActionInProgress) {
return;
}
+ if (!(await DialogUtils.confirm(this.$t("about.recovery_confirm")))) {
+ return;
+ }
this.databaseActionInProgress = true;
this.databaseActionMessage = "";
this.databaseActionError = "";
@@ -1335,9 +1342,13 @@ export default {
this.databaseHealth = response.data.database.health;
}
this.databaseRecoveryActions = response.data.database?.actions || [];
- this.databaseActionMessage = response.data.message || "Database recovery completed";
+ const msg = response.data.message || this.$t("about.recovery_complete");
+ this.databaseActionMessage = msg;
+ ToastUtils.success(this.$t("about.recovery_complete"));
} catch (e) {
this.databaseActionError = this.$t("about.recovery_failed");
+ const detail = e?.response?.data?.message;
+ ToastUtils.error(detail || this.$t("about.recovery_failed"));
console.log(e);
} finally {
this.databaseActionInProgress = false;
@@ -1471,6 +1482,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.about-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8 last:border-0;
}
diff --git a/meshchatx/src/frontend/components/archives/ArchiveSidebar.vue b/meshchatx/src/frontend/components/archives/ArchiveSidebar.vue
index aafcf752..1c67cfe1 100644
--- a/meshchatx/src/frontend/components/archives/ArchiveSidebar.vue
+++ b/meshchatx/src/frontend/components/archives/ArchiveSidebar.vue
@@ -28,7 +28,7 @@
v-model="searchQuery"
type="text"
placeholder="Search nodes or content..."
- class="w-full pl-9 pr-3 py-1.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-1 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all"
+ class="w-full pl-9 pr-3 py-1.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all"
@input="$emit('update:search-query', searchQuery)"
/>
<button
diff --git a/meshchatx/src/frontend/components/archives/ArchivesPage.vue b/meshchatx/src/frontend/components/archives/ArchivesPage.vue
index 30856ba7..1de234b5 100644
--- a/meshchatx/src/frontend/components/archives/ArchivesPage.vue
+++ b/meshchatx/src/frontend/components/archives/ArchivesPage.vue
@@ -34,7 +34,7 @@
{{ selectedNode.node_name }}
</h2>
<div
- class="text-[10px] font-bold px-1.5 py-0.5 bg-gray-200 dark:bg-zinc-700 text-gray-600 dark:text-gray-400 rounded"
+ class="text-[10px] font-bold px-1.5 py-0.5 bg-gray-200 dark:bg-zinc-700 text-gray-600 dark:text-gray-400 rounded-sm"
>
{{ selectedNode.archives.length }}
</div>
@@ -46,7 +46,7 @@
<label class="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
- class="rounded border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
+ class="rounded-sm border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
:checked="isAllSelected"
@change="toggleSelectAll"
/>
@@ -95,7 +95,7 @@
<input
v-model="selectedArchives"
type="checkbox"
- class="rounded border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
+ class="rounded-sm border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
:value="archive.id"
/>
</div>
@@ -155,7 +155,7 @@
<div class="flex items-center gap-1">
<button
- class="hidden rounded p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
+ class="hidden rounded-sm p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
:class="{
'text-blue-600 dark:text-blue-400': !isSidebar1Hidden,
'text-gray-400 dark:text-zinc-500': isSidebar1Hidden,
@@ -166,7 +166,7 @@
<MaterialDesignIcon icon-name="page-layout-sidebar-left" class="size-4" />
</button>
<button
- class="hidden rounded p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
+ class="hidden rounded-sm p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
:class="{
'text-blue-600 dark:text-blue-400': !isSidebar2Hidden,
'text-gray-400 dark:text-zinc-500': isSidebar2Hidden,
@@ -178,7 +178,7 @@
</button>
<div class="mx-1 hidden h-6 w-px bg-gray-200 dark:bg-zinc-800 sm:block"></div>
<button
- class="flex items-center gap-2 rounded p-2 text-gray-700 transition-colors hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
+ class="flex items-center gap-2 rounded-sm p-2 text-gray-700 transition-colors hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
:title="$t('archives.export_mu')"
@click="exportArchiveAsMu(viewingArchive)"
>
@@ -189,7 +189,7 @@
</button>
<div class="mx-1 hidden h-6 w-px bg-gray-200 dark:bg-zinc-800 xs:block"></div>
<button
- class="flex items-center gap-2 rounded p-2 text-blue-600 transition-colors hover:bg-gray-100 dark:text-blue-400 dark:hover:bg-zinc-800"
+ class="flex items-center gap-2 rounded-sm p-2 text-blue-600 transition-colors hover:bg-gray-100 dark:text-blue-400 dark:hover:bg-zinc-800"
@click="openInNomadnet(viewingArchive)"
>
<MaterialDesignIcon icon-name="open-in-new" class="size-4" />
@@ -197,7 +197,7 @@
</button>
<div class="mx-1 hidden h-6 w-px bg-gray-200 dark:bg-zinc-800 xs:block"></div>
<button
- class="hidden rounded p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
+ class="hidden rounded-sm p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
title="Close"
@click="viewingArchive = null"
>
@@ -284,13 +284,13 @@ export default {
archiveViewerClasses() {
const a = this.viewingArchive;
if (!a?.page_path) {
- return ["break-words", "whitespace-pre-wrap", "text-gray-100"];
+ return ["wrap-break-word", "whitespace-pre-wrap", "text-gray-100"];
}
const pl = (a.page_path || "").split("`")[0].toLowerCase();
const isRich = pl.endsWith(".mu") || pl.endsWith(".md") || pl.endsWith(".html");
const isHtml = pl.endsWith(".html");
const isMd = pl.endsWith(".md");
- const classes = ["break-words"];
+ const classes = ["wrap-break-word"];
if (isRich) {
classes.push("nomad-page-rich");
} else {
diff --git a/meshchatx/src/frontend/components/auth/AuthPage.vue b/meshchatx/src/frontend/components/auth/AuthPage.vue
index 1deffdb8..4930765a 100644
--- a/meshchatx/src/frontend/components/auth/AuthPage.vue
+++ b/meshchatx/src/frontend/components/auth/AuthPage.vue
@@ -35,7 +35,7 @@
type="password"
required
minlength="8"
- class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter password"
autocomplete="current-password"
/>
@@ -57,7 +57,7 @@
type="password"
required
minlength="8"
- class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Confirm password"
autocomplete="new-password"
/>
diff --git a/meshchatx/src/frontend/components/blocked/BlockedPage.vue b/meshchatx/src/frontend/components/blocked/BlockedPage.vue
index 369fdd77..30eb53dc 100644
--- a/meshchatx/src/frontend/components/blocked/BlockedPage.vue
+++ b/meshchatx/src/frontend/components/blocked/BlockedPage.vue
@@ -3,7 +3,7 @@
<template>
<div class="flex flex-col flex-1 h-full overflow-hidden bg-slate-50 dark:bg-zinc-950">
<div
- class="flex items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-sm"
+ class="flex items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-xs"
>
<div class="flex items-center gap-3">
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg">
@@ -23,7 +23,7 @@
<input
v-model="searchQuery"
type="text"
- class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
+ class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
:placeholder="$t('banishment.search_placeholder')"
@input="onSearchInput"
/>
@@ -71,7 +71,7 @@
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
- <div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg flex-shrink-0">
+ <div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg shrink-0">
<MaterialDesignIcon
icon-name="account-off"
class="size-5 text-red-600 dark:text-red-400"
@@ -80,26 +80,26 @@
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<h4
- class="text-base font-semibold text-gray-900 dark:text-white break-words"
+ class="text-base font-semibold text-gray-900 dark:text-white wrap-break-word"
:title="item.display_name"
>
{{ item.display_name || $t("call.unknown") }}
</h4>
<span
v-if="item.is_node"
- class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded"
+ class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-sm"
>
{{ $t("banishment.node") }}
</span>
<span
v-else
- class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded"
+ class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-sm"
>
{{ $t("banishment.user") }}
</span>
<span
v-if="item.is_rns_blackholed"
- class="px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded border border-zinc-200 dark:border-zinc-700"
+ class="px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded-sm border border-zinc-200 dark:border-zinc-700"
title="Blackholed at Reticulum transport layer"
>
RNS Blackhole
diff --git a/meshchatx/src/frontend/components/call/CallOverlay.vue b/meshchatx/src/frontend/components/call/CallOverlay.vue
index daa57cce..54636006 100644
--- a/meshchatx/src/frontend/components/call/CallOverlay.vue
+++ b/meshchatx/src/frontend/components/call/CallOverlay.vue
@@ -3,7 +3,7 @@
<template>
<div
v-if="activeCall || initiationStatus || isEnded || wasDeclined"
- class="fixed bottom-4 right-4 z-[100] w-80 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
+ class="fixed bottom-4 right-4 z-100 w-80 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
:class="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded || wasDeclined }"
>
<!-- Header -->
@@ -221,7 +221,7 @@
class="p-2.5 rounded-full bg-red-600 text-white hover:bg-red-700 shadow-lg shadow-red-600/30 transition-all duration-200"
@click="hangupCall(null)"
>
- <MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
+ <MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-135" />
</button>
<!-- Send to Voicemail (if incoming) -->
@@ -290,7 +290,7 @@
<div class="flex items-center space-x-1">
<button
type="button"
- class="p-1.5 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded transition-colors"
+ class="p-1.5 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-sm transition-colors"
@click="toggleMicrophone"
>
<MaterialDesignIcon
@@ -301,10 +301,10 @@
</button>
<button
type="button"
- class="p-1.5 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors"
+ class="p-1.5 hover:bg-red-100 dark:hover:bg-red-900/30 rounded-sm transition-colors"
@click="hangupCall"
>
- <MaterialDesignIcon icon-name="phone-hangup" class="size-4 text-red-500 rotate-[135deg]" />
+ <MaterialDesignIcon icon-name="phone-hangup" class="size-4 text-red-500 rotate-135" />
</button>
</div>
</div>
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index a5f578ad..42d64911 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -86,7 +86,7 @@
class="flex-1 flex flex-col items-center justify-center py-12 px-4"
>
<div
- class="w-full max-w-md border-b border-gray-200 dark:border-zinc-800 !p-8 flex flex-col items-center text-center relative overflow-hidden"
+ class="w-full max-w-md border-b border-gray-200 dark:border-zinc-800 p-8! flex flex-col items-center text-center relative overflow-hidden"
>
<!-- Status pulse background -->
<div
@@ -279,7 +279,7 @@
<div class="flex flex-col gap-4">
<select
v-model="selectedAudioProfileId"
- class="input-field !rounded-xl !py-2 shadow-sm"
+ class="input-field rounded-xl! py-2! shadow-xs"
@change="switchAudioProfile(selectedAudioProfileId)"
>
<option
@@ -375,7 +375,7 @@
class="w-full flex items-center justify-center gap-2 rounded-2xl bg-red-600 py-4 text-sm font-bold text-white shadow-xl shadow-red-600/20 hover:bg-red-500 transition-all duration-200"
@click="hangupCall"
>
- <MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
+ <MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-135" />
<span>{{
activeCall && activeCall.is_incoming && activeCall.status === 4
? $t("call.decline")
@@ -549,7 +549,7 @@
<select
v-if="config"
v-model="config.telephone_audio_profile_id"
- class="input-field min-w-0 !rounded-lg !border-gray-200 !py-1 !px-2 !text-xs dark:!border-zinc-800 lg:min-w-[120px]"
+ class="input-field min-w-0 rounded-lg! border-gray-200! py-1! px-2! text-xs! dark:border-zinc-800! lg:min-w-[120px]"
@change="
updateConfig({
telephone_audio_profile_id: config.telephone_audio_profile_id,
@@ -579,7 +579,7 @@
</div>
<select
v-model="selectedAudioInputId"
- class="input-field !py-1 !px-2 !text-[10px] !rounded-lg !border-gray-200 dark:!border-zinc-800 min-w-[120px]"
+ class="input-field py-1! px-2! text-[10px]! rounded-lg! border-gray-200! dark:border-zinc-800! min-w-[120px]"
@change="
stopWebAudio();
startWebAudio();
@@ -602,7 +602,7 @@
</div>
<select
v-model="selectedAudioOutputId"
- class="input-field !py-1 !px-2 !text-[10px] !rounded-lg !border-gray-200 dark:!border-zinc-800 min-w-[120px]"
+ class="input-field py-1! px-2! text-[10px]! rounded-lg! border-gray-200! dark:border-zinc-800! min-w-[120px]"
@change="
stopWebAudio();
startWebAudio();
@@ -636,7 +636,7 @@
v-if="callHistory.length > 0 && !activeCall && !isCallEnded"
class="space-y-4 max-w-3xl mx-auto w-full"
>
- <div class="w-full border-b border-gray-200 dark:border-zinc-800 !p-0 overflow-hidden">
+ <div class="w-full border-b border-gray-200 dark:border-zinc-800 p-0! overflow-hidden">
<div
class="px-5 py-4 border-b border-gray-100 dark:border-zinc-800 flex flex-col gap-4 bg-transparent"
>
@@ -667,7 +667,7 @@
v-model="callHistorySearch"
type="text"
:placeholder="$t('call.search_history')"
- class="input-field !py-2 !pl-10"
+ class="input-field py-2! pl-10!"
@input="onCallHistorySearchInput"
/>
<MaterialDesignIcon
@@ -699,7 +699,7 @@
icon-class="size-10"
/>
<div
- class="absolute -bottom-1 -right-1 bg-white dark:bg-zinc-900 rounded-full p-0.5 shadow-sm border border-gray-100 dark:border-zinc-800 shrink-0 flex items-center justify-center size-5"
+ class="absolute -bottom-1 -right-1 bg-white dark:bg-zinc-900 rounded-full p-0.5 shadow-xs border border-gray-100 dark:border-zinc-800 shrink-0 flex items-center justify-center size-5"
>
<MaterialDesignIcon
:icon-name="entry.is_incoming ? 'phone-incoming' : 'phone-outgoing'"
@@ -762,7 +762,7 @@
</div>
<div
- class="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4"
+ class="flex items-center gap-1.5 opacity-100 transition-opacity shrink-0 ml-4 lg:opacity-0 lg:group-hover:opacity-100"
>
<button
v-if="!entry.is_contact"
@@ -837,7 +837,7 @@
v-model="discoverySearch"
type="text"
:placeholder="`Search phonebook (${totalDiscoveryCount})...`"
- class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onDiscoverySearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -953,7 +953,7 @@
v-model="voicemailSearch"
type="text"
placeholder="Search voicemails..."
- class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onVoicemailSearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -1012,7 +1012,7 @@
</div>
<button
:disabled="!voicemailStatus.has_espeak"
- class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
+ class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden disabled:opacity-50 disabled:cursor-not-allowed"
:class="config.voicemail_enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-zinc-700'"
@click="
config.voicemail_enabled = !config.voicemail_enabled;
@@ -1020,7 +1020,7 @@
"
>
<span
- class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
:class="config.voicemail_enabled ? 'translate-x-5' : 'translate-x-0'"
></span>
</button>
@@ -1035,7 +1035,7 @@
<textarea
v-model="config.voicemail_greeting"
rows="3"
- class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-900"
placeholder="Enter greeting text..."
></textarea>
@@ -1051,7 +1051,7 @@
type="number"
min="80"
max="450"
- class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="updateConfig({ voicemail_tts_speed: config.voicemail_tts_speed })"
/>
</div>
@@ -1065,7 +1065,7 @@
type="number"
min="0"
max="99"
- class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="updateConfig({ voicemail_tts_pitch: config.voicemail_tts_pitch })"
/>
</div>
@@ -1079,7 +1079,7 @@
type="number"
min="0"
max="100"
- class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="
updateConfig({ voicemail_tts_word_gap: config.voicemail_tts_word_gap })
"
@@ -1093,7 +1093,7 @@
<input
v-model="config.voicemail_tts_voice"
type="text"
- class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="updateConfig({ voicemail_tts_voice: config.voicemail_tts_voice })"
/>
</div>
@@ -1202,7 +1202,7 @@
type="number"
min="1"
max="120"
- class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@change="
updateConfig({
voicemail_auto_answer_delay_seconds:
@@ -1221,7 +1221,7 @@
type="number"
min="5"
max="600"
- class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@change="
updateConfig({
voicemail_max_recording_seconds: config.voicemail_max_recording_seconds,
@@ -1373,7 +1373,7 @@
v-model="contactsSearch"
type="text"
placeholder="Search contacts..."
- class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onContactsSearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -1382,7 +1382,7 @@
</div>
<button
type="button"
- class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-colors flex items-center gap-2"
+ class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-blue-500 transition-colors flex items-center gap-2"
@click="openAddContactModal"
>
<MaterialDesignIcon icon-name="plus" class="size-5" />
@@ -1428,7 +1428,7 @@
<div class="flex items-center gap-2">
<span
v-if="contact.preferred_ringtone_id"
- class="text-[9px] px-1.5 py-0.5 rounded bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 border border-amber-100 dark:border-amber-800/50 flex items-center gap-1"
+ class="text-[9px] px-1.5 py-0.5 rounded-sm bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 border border-amber-100 dark:border-amber-800/50 flex items-center gap-1"
title="Custom Ringtone Set"
>
<MaterialDesignIcon icon-name="music" class="size-2.5" />
@@ -1528,7 +1528,7 @@
{{ $t("call.enable_custom_ringtone") }}
</div>
<button
- class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
+ class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden"
:class="
config.custom_ringtone_enabled
? 'bg-blue-600'
@@ -1542,7 +1542,7 @@
"
>
<span
- class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
:class="
config.custom_ringtone_enabled
? 'translate-x-5'
@@ -1588,7 +1588,7 @@
Tone Generator
</div>
<button
- class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
+ class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden"
:class="
config.telephone_tone_generator_enabled
? 'bg-blue-600'
@@ -1604,7 +1604,7 @@
"
>
<span
- class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
:class="
config.telephone_tone_generator_enabled
? 'translate-x-5'
@@ -1660,7 +1660,7 @@
</div>
<select
v-model="config.ringtone_preferred_id"
- class="input-field !py-1.5 !px-3 !text-sm !rounded-xl !border-gray-200 dark:!border-zinc-800 min-w-[200px]"
+ class="input-field py-1.5! px-3! text-sm! rounded-xl! border-gray-200! dark:border-zinc-800! min-w-[200px]"
@change="
updateConfig({ ringtone_preferred_id: config.ringtone_preferred_id })
"
@@ -1716,7 +1716,7 @@
>
<input
v-model="editingRingtoneName"
- class="text-sm bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded px-2 py-1 flex-1 min-w-0"
+ class="text-sm bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-sm px-2 py-1 flex-1 min-w-0"
@keyup.enter="saveRingtoneName"
@blur="saveRingtoneName"
/>
@@ -1730,7 +1730,7 @@
</span>
<span
v-if="ringtone.is_primary"
- class="shrink-0 text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 rounded"
+ class="shrink-0 text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 rounded-sm"
>
Primary
</span>
@@ -1826,7 +1826,7 @@
v-model="recordingSearch"
type="text"
placeholder="Search recordings..."
- class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onRecordingSearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -1962,7 +1962,7 @@
<!-- Contact Modal -->
<div
v-if="isContactModalOpen"
- class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity"
+ class="fixed inset-0 z-150 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs transition-opacity"
@click.self="isContactModalOpen = false"
>
<div
@@ -2106,7 +2106,7 @@
</button>
<button
type="button"
- class="flex-[2] px-6 py-3 rounded-2xl bg-blue-600 text-white font-bold shadow-lg shadow-blue-600/20 hover:bg-blue-500 transition-all active:scale-95"
+ class="flex-2 px-6 py-3 rounded-2xl bg-blue-600 text-white font-bold shadow-lg shadow-blue-600/20 hover:bg-blue-500 transition-all active:scale-95"
@click="saveContact(contactForm)"
>
{{ $t("call.save_contact") }}
diff --git a/meshchatx/src/frontend/components/call/RingtoneEditor.vue b/meshchatx/src/frontend/components/call/RingtoneEditor.vue
index 67c2a9e6..85407d7a 100644
--- a/meshchatx/src/frontend/components/call/RingtoneEditor.vue
+++ b/meshchatx/src/frontend/components/call/RingtoneEditor.vue
@@ -116,7 +116,7 @@
id="saveAsNew"
v-model="saveAsNew"
type="checkbox"
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label for="saveAsNew" class="text-sm text-gray-600 dark:text-zinc-400 cursor-pointer"
>Save as new ringtone</label
diff --git a/meshchatx/src/frontend/components/call/RingtoneEditorModal.vue b/meshchatx/src/frontend/components/call/RingtoneEditorModal.vue
index 9e477aad..75379424 100644
--- a/meshchatx/src/frontend/components/call/RingtoneEditorModal.vue
+++ b/meshchatx/src/frontend/components/call/RingtoneEditorModal.vue
@@ -1,8 +1,8 @@
<!-- SPDX-License-Identifier: 0BSD -->
<template>
- <div class="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6">
- <div class="absolute inset-0 bg-zinc-900/80 backdrop-blur-sm" @click="$emit('close')"></div>
+ <div class="fixed inset-0 z-100 flex items-center justify-center p-4 sm:p-6">
+ <div class="absolute inset-0 bg-zinc-900/80 backdrop-blur-xs" @click="$emit('close')"></div>
<div
class="relative w-full max-w-4xl bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
>
@@ -131,7 +131,7 @@
id="saveAsNew"
v-model="saveAsNew"
type="checkbox"
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label for="saveAsNew" class="text-sm text-gray-600 dark:text-zinc-400 cursor-pointer"
>Save as new ringtone</label
diff --git a/meshchatx/src/frontend/components/contacts/ContactsPage.vue b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
index da9ce60b..8e090186 100644
--- a/meshchatx/src/frontend/components/contacts/ContactsPage.vue
+++ b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
@@ -54,7 +54,7 @@
v-model="contactsSearch"
type="text"
:placeholder="$t('contacts.search_placeholder')"
- class="input-field !pl-11"
+ class="input-field pl-11!"
@input="onContactsSearchInput"
/>
</div>
@@ -71,8 +71,8 @@
class="size-10 sm:size-12 rounded-full bg-gray-200 dark:bg-zinc-700 animate-pulse shrink-0"
/>
<div class="flex-1 min-w-0 space-y-2">
- <div class="h-4 w-32 bg-gray-200 dark:bg-zinc-700 rounded animate-pulse" />
- <div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded animate-pulse" />
+ <div class="h-4 w-32 bg-gray-200 dark:bg-zinc-700 rounded-sm animate-pulse" />
+ <div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded-sm animate-pulse" />
</div>
</div>
</template>
@@ -89,7 +89,7 @@
class="group flex cursor-default items-center gap-3 px-1 py-3 transition-colors hover:bg-gray-50/80 dark:hover:bg-zinc-900/70"
@contextmenu.prevent="openContextMenu($event, contact)"
>
- <div class="flex-shrink-0">
+ <div class="shrink-0">
<LxmfUserIcon
:custom-image="contact.custom_image"
:icon-name="contact.remote_icon ? contact.remote_icon.icon_name : ''"
@@ -110,7 +110,9 @@
{{ contact.lxmf_address || contact.remote_identity_hash }}
</div>
</div>
- <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
+ <div
+ class="flex items-center gap-1 opacity-100 transition-opacity lg:opacity-0 lg:group-hover:opacity-100"
+ >
<button
type="button"
class="p-1.5 rounded-lg text-gray-500 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
@@ -154,7 +156,7 @@
<button
type="button"
- class="sm:hidden fixed bottom-5 right-4 z-[180] flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
+ class="sm:hidden fixed bottom-5 right-4 z-180 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
:title="$t('contacts.add_contact')"
@click="openAddDialog"
>
@@ -162,7 +164,7 @@
</button>
<!-- Contact context menu -->
- <ContextMenuPanel :show="contextMenu.visible" :x="contextMenu.x" :y="contextMenu.y" panel-class="z-[210]">
+ <ContextMenuPanel :show="contextMenu.visible" :x="contextMenu.x" :y="contextMenu.y" panel-class="z-210">
<ContextMenuItem @click="openConversation(contextMenu.contact)">
<MaterialDesignIcon icon-name="message-text-outline" class="size-4" />
{{ $t("contacts.send_message") }}
@@ -194,7 +196,7 @@
<!-- Add contact dialog -->
<div
v-if="isAddDialogOpen"
- class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-200 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="closeAddDialog"
>
<div class="w-full max-w-lg rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -229,7 +231,7 @@
v-model="newContactInput"
type="text"
class="input-field font-mono"
- :class="cameraSupported ? '!pr-12' : ''"
+ :class="cameraSupported ? 'pr-12!' : ''"
:placeholder="$t('contacts.hash_or_uri_placeholder')"
@keydown.enter.prevent="submitAddContact"
/>
@@ -279,7 +281,7 @@
<!-- Scanner dialog -->
<div
v-if="isScannerDialogOpen"
- class="fixed inset-0 z-[220] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
+ class="fixed inset-0 z-220 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs"
@click.self="closeScannerDialog"
>
<div class="w-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -311,7 +313,7 @@
<!-- Import contacts dialog -->
<div
v-if="isImportDialogOpen"
- class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-200 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="closeImportDialog"
>
<div class="w-full max-w-lg rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -354,7 +356,7 @@
<!-- My identity dialog -->
<div
v-if="isMyIdentityDialogOpen"
- class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-200 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="isMyIdentityDialogOpen = false"
>
<div class="w-full max-w-md rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -923,8 +925,9 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.glass-card {
- @apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-sm p-4;
+ @apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xs p-4;
}
.input-field {
diff --git a/meshchatx/src/frontend/components/debug/DebugLogsPage.vue b/meshchatx/src/frontend/components/debug/DebugLogsPage.vue
index 39ec61af..76b48cba 100644
--- a/meshchatx/src/frontend/components/debug/DebugLogsPage.vue
+++ b/meshchatx/src/frontend/components/debug/DebugLogsPage.vue
@@ -63,7 +63,7 @@
<input
v-model="search"
type="text"
- class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+ class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
:placeholder="$t('debug.search_logs_placeholder')"
@input="debouncedSearch"
/>
@@ -71,7 +71,7 @@
<select
v-model="level"
- class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
+ class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-hidden focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
@change="refreshLogs"
>
<option value="">{{ $t("debug.level_all") }}</option>
@@ -106,14 +106,14 @@
<input
v-model="accessSearch"
type="text"
- class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
+ class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
:placeholder="$t('debug.search_access_placeholder')"
@input="debouncedAccessSearch"
/>
</div>
<select
v-model="accessOutcome"
- class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
+ class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-hidden focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
@change="refreshAccessAttempts"
>
<option value="">{{ $t("debug.outcome_all") }}</option>
@@ -130,7 +130,7 @@
</div>
</div>
- <div class="flex-1 overflow-hidden glass-card max-w-6xl mx-auto w-full p-0 flex flex-col rounded-sm">
+ <div class="flex-1 overflow-hidden glass-card max-w-6xl mx-auto w-full p-0 flex flex-col rounded-xs">
<div
v-if="activeTab === 'logs'"
class="flex-1 overflow-auto p-3 sm:p-4 font-mono text-xs leading-relaxed select-text bg-white dark:bg-zinc-950"
@@ -153,7 +153,7 @@
<span class="text-blue-500 shrink-0 w-20 sm:w-24 overflow-hidden text-ellipsis italic"
>[{{ log.module }}]</span
>
- <span class="text-gray-800 dark:text-gray-200 break-words flex-1">
+ <span class="text-gray-800 dark:text-gray-200 wrap-break-word flex-1">
{{ log.message }}
<span
v-if="log.is_anomaly"
diff --git a/meshchatx/src/frontend/components/docs/DocsPage.vue b/meshchatx/src/frontend/components/docs/DocsPage.vue
index 102c19a6..b2eefb32 100644
--- a/meshchatx/src/frontend/components/docs/DocsPage.vue
+++ b/meshchatx/src/frontend/components/docs/DocsPage.vue
@@ -30,7 +30,7 @@
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'meshchatx'
- ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
+ ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'meshchatx'"
@@ -41,7 +41,7 @@
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'reticulum'
- ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
+ ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'reticulum'"
@@ -58,7 +58,7 @@
<input
v-model="searchQuery"
type="text"
- class="block w-full pl-8 pr-8 py-1.5 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-[11px] focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
+ class="block w-full pl-8 pr-8 py-1.5 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-[11px] focus:outline-hidden focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
placeholder="Search documentation..."
@input="debounceSearch"
/>
@@ -222,7 +222,7 @@
v-if="status.has_docs"
:href="localDocsUrl"
target="_blank"
- class="hidden sm:flex items-center px-2.5 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg hover:opacity-90 transition-opacity font-bold text-[10px] shadow-sm"
+ class="hidden sm:flex items-center px-2.5 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg hover:opacity-90 transition-opacity font-bold text-[10px] shadow-xs"
>
<MaterialDesignIcon icon-name="open-in-new" class="w-3 h-3 mr-1.5" />
Open
@@ -242,7 +242,7 @@
class="flex-1 md:flex-none px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'meshchatx'
- ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
+ ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'meshchatx'"
@@ -253,7 +253,7 @@
class="flex-1 md:flex-none px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'reticulum'
- ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
+ ? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'reticulum'"
@@ -270,7 +270,7 @@
<input
v-model="searchQuery"
type="text"
- class="block w-full pl-9 pr-9 py-2 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
+ class="block w-full pl-9 pr-9 py-2 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-xs focus:outline-hidden focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
placeholder="Search all documentation..."
@input="debounceSearch"
/>
@@ -330,7 +330,7 @@
</div>
<div class="flex items-center space-x-2">
<span
- class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-zinc-800 text-[8px] font-bold text-gray-500 uppercase tracking-tighter"
+ class="px-1.5 py-0.5 rounded-sm bg-gray-100 dark:bg-zinc-800 text-[8px] font-bold text-gray-500 uppercase tracking-tighter"
>
{{ result.source }}
</span>
@@ -370,7 +370,7 @@
<div
v-if="status.last_error"
- class="absolute inset-0 z-10 flex items-center justify-center p-6 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-sm"
+ class="absolute inset-0 z-10 flex items-center justify-center p-6 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-xs"
>
<div
class="max-w-md w-full p-6 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-900/30 rounded-2xl text-red-600 dark:text-red-400 text-center shadow-xl"
@@ -436,7 +436,7 @@
class="w-full text-left px-3 py-2 rounded-xl text-xs transition-all flex items-center space-x-3"
:class="
selectedDocPath === doc.path
- ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-bold shadow-sm'
+ ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-bold shadow-xs'
: 'text-gray-600 dark:text-zinc-400 hover:bg-white dark:hover:bg-zinc-800'
"
@click="selectDoc(doc.path)"
@@ -467,7 +467,7 @@
<div v-if="selectedDocContent" class="flex-1 overflow-y-auto p-6 md:p-10 scroll-smooth">
<div class="max-w-3xl mx-auto">
- <div class="max-w-none break-words" v-html="selectedDocContent.html"></div>
+ <div class="max-w-none wrap-break-word" v-html="selectedDocContent.html"></div>
</div>
</div>
<div
@@ -786,7 +786,7 @@ export default {
const regex = new RegExp(`(${query})`, "gi");
return escapedText.replace(
regex,
- '<span class="bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-0.5 rounded">$1</span>'
+ '<span class="bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-0.5 rounded-sm">$1</span>'
);
},
},
diff --git a/meshchatx/src/frontend/components/forms/Toggle.vue b/meshchatx/src/frontend/components/forms/Toggle.vue
index cc7d30b3..7852e591 100644
--- a/meshchatx/src/frontend/components/forms/Toggle.vue
+++ b/meshchatx/src/frontend/components/forms/Toggle.vue
@@ -15,7 +15,7 @@
@change="!disabled && $emit('update:modelValue', $event.target.checked)"
/>
<div
- class="relative h-6 w-11 shrink-0 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-zinc-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"
+ class="relative h-6 w-11 shrink-0 bg-gray-200 peer-focus:outline-hidden peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-zinc-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"
></div>
<span v-if="label" class="min-w-0 text-sm font-medium leading-snug text-gray-900 dark:text-gray-300">{{
label
diff --git a/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue b/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue
index dffed938..a49aa35a 100644
--- a/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue
+++ b/meshchatx/src/frontend/components/forwarder/ForwarderPage.vue
@@ -29,7 +29,7 @@
v-model="newRule.name"
type="text"
:placeholder="$t('forwarder.name_placeholder')"
- class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-none"
+ class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-hidden"
/>
</div>
<div class="space-y-1">
@@ -41,7 +41,7 @@
v-model="newRule.forward_to_hash"
type="text"
:placeholder="$t('forwarder.destination_placeholder')"
- class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-none"
+ class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-hidden"
/>
</div>
<div class="space-y-1">
@@ -53,7 +53,7 @@
v-model="newRule.source_filter_hash"
type="text"
:placeholder="$t('forwarder.source_filter_placeholder')"
- class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-none"
+ class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-hidden"
/>
</div>
</div>
@@ -87,7 +87,7 @@
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<div
- class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider"
+ class="px-2 py-0.5 rounded-sm text-[10px] font-bold uppercase tracking-wider"
:class="
rule.is_active
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
index 727b0b52..0da939a6 100644
--- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
+++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
@@ -155,7 +155,7 @@
<div class="mt-3">
<select
v-model="newInterfaceType"
- class="input-field appearance-none pr-10 !py-1.5 !text-[11px] opacity-70 hover:opacity-100"
+ class="input-field appearance-none pr-10 py-1.5! text-[11px]! opacity-70 hover:opacity-100"
>
<option :value="null">More options...</option>
<option value="AX25KISSInterface">AX.25 KISS (Amateur Radio)</option>
@@ -218,7 +218,7 @@
id="tcp-kiss-framing"
v-model="newInterfaceKISSFramingEnabled"
/>
- <FormLabel for="tcp-kiss-framing" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="tcp-kiss-framing" class="cursor-pointer mb-0! text-sm"
>Use KISS framing (legacy compatibility)</FormLabel
>
</div>
@@ -227,10 +227,25 @@
id="tcp-i2p-tunneled"
v-model="newInterfaceI2PTunnelingEnabled"
/>
- <FormLabel for="tcp-i2p-tunneled" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="tcp-i2p-tunneled" class="cursor-pointer mb-0! text-sm"
>I2P Tunneled (target is an I2P b32)</FormLabel
>
</div>
+ <div class="flex items-start gap-2">
+ <Toggle id="tcp-bootstrap-only" v-model="newInterfaceBootstrapOnly" />
+ <div class="min-w-0">
+ <FormLabel
+ for="tcp-bootstrap-only"
+ class="cursor-pointer mb-0! text-sm block"
+ >{{
+ $t("interfaces.discovery_default_bootstrap_only")
+ }}</FormLabel
+ >
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
+ {{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
+ </p>
+ </div>
+ </div>
<div class="grid grid-cols-3 gap-3">
<div>
<FormLabel class="glass-label">Connect Timeout (s)</FormLabel>
@@ -273,7 +288,7 @@
/>
<FormLabel
for="backbone-listen-mode"
- class="cursor-pointer !mb-0 text-sm"
+ class="cursor-pointer mb-0! text-sm"
>Listener mode (host this backbone)</FormLabel
>
</div>
@@ -305,6 +320,24 @@
class="input-field font-mono text-xs"
/>
</div>
+ <div class="flex items-start gap-2">
+ <Toggle
+ id="backbone-bootstrap-only"
+ v-model="newInterfaceBootstrapOnly"
+ />
+ <div class="min-w-0">
+ <FormLabel
+ for="backbone-bootstrap-only"
+ class="cursor-pointer mb-0! text-sm block"
+ >{{
+ $t("interfaces.discovery_default_bootstrap_only")
+ }}</FormLabel
+ >
+ <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
+ {{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
+ </p>
+ </div>
+ </div>
</div>
<div v-else class="space-y-4">
<div>
@@ -341,7 +374,7 @@
/>
<FormLabel
for="backbone-listen-ipv6"
- class="cursor-pointer !mb-0 text-sm"
+ class="cursor-pointer mb-0! text-sm"
>Prefer IPv6</FormLabel
>
</div>
@@ -388,7 +421,7 @@
<Toggle id="tcp-server-ipv6" v-model="newInterfacePreferIPV6" />
<FormLabel
for="tcp-server-ipv6"
- class="cursor-pointer !mb-0 text-sm"
+ class="cursor-pointer mb-0! text-sm"
>Prefer IPv6</FormLabel
>
</div>
@@ -397,7 +430,7 @@
id="tcp-server-i2p"
v-model="newInterfaceI2PTunnelingEnabled"
/>
- <FormLabel for="tcp-server-i2p" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="tcp-server-i2p" class="cursor-pointer mb-0! text-sm"
>I2P Tunneled</FormLabel
>
</div>
@@ -436,7 +469,7 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="i2p-connectable" v-model="newInterfaceConnectable" />
- <FormLabel for="i2p-connectable" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="i2p-connectable" class="cursor-pointer mb-0! text-sm"
>Allow incoming peers (connectable)</FormLabel
>
</div>
@@ -467,7 +500,7 @@
</div>
<button
type="button"
- class="secondary-chip !py-1 !px-3 !text-[10px]"
+ class="secondary-chip py-1! px-3! text-[10px]!"
@click="addI2PPeer('')"
>
<MaterialDesignIcon icon-name="plus" class="size-3" /> Add Peer
@@ -494,7 +527,7 @@
class="flex items-center gap-2 pb-2"
>
<Toggle id="rnode-use-ip" v-model="newInterfaceRNodeUseIP" />
- <FormLabel for="rnode-use-ip" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="rnode-use-ip" class="cursor-pointer mb-0! text-sm"
>Connect over network (IP)</FormLabel
>
</div>
@@ -655,7 +688,7 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="rnode-flow-control" v-model="newInterfaceFlowControl" />
- <FormLabel for="rnode-flow-control" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="rnode-flow-control" class="cursor-pointer mb-0! text-sm"
>Hardware flow control</FormLabel
>
</div>
@@ -810,7 +843,7 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="kiss-flow-control" v-model="newInterfaceFlowControl" />
- <FormLabel for="kiss-flow-control" class="cursor-pointer !mb-0 text-sm"
+ <FormLabel for="kiss-flow-control" class="cursor-pointer mb-0! text-sm"
>Hardware flow control</FormLabel
>
</div>
@@ -1003,7 +1036,7 @@
<!-- RNode Advanced Tools -->
<ExpandingSection
v-if="['RNodeInterface', 'RNodeIPInterface'].includes(newInterfaceType)"
- class="glass-card !p-0 overflow-hidden"
+ class="glass-card p-0! overflow-hidden"
>
<template #title
><span class="text-sm font-bold">Calculated Parameters</span></template
@@ -1055,7 +1088,7 @@
</ExpandingSection>
<!-- Interface Discovery Settings -->
- <ExpandingSection class="glass-card !p-0 overflow-hidden">
+ <ExpandingSection class="glass-card p-0! overflow-hidden">
<template #title
><span class="text-sm font-bold">Interface Discovery</span></template
>
@@ -1063,7 +1096,7 @@
<div class="p-6 space-y-6">
<div class="flex items-center justify-between">
<div class="max-w-md">
- <FormLabel class="glass-label !mb-0"
+ <FormLabel class="glass-label mb-0!"
>Publish Discovery Announce</FormLabel
>
<p class="text-xs text-gray-400">
@@ -1157,13 +1190,13 @@
</div>
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center justify-between gap-4 max-w-md">
- <FormLabel class="glass-label !mb-0"
+ <FormLabel class="glass-label mb-0!"
>Encrypt discovery</FormLabel
>
<Toggle v-model="discovery.discovery_encrypt" />
</div>
<div class="flex items-center justify-between gap-4 max-w-md">
- <FormLabel class="glass-label !mb-0"
+ <FormLabel class="glass-label mb-0!"
>Publish IFAC in announce</FormLabel
>
<Toggle v-model="discovery.publish_ifac" />
@@ -1175,7 +1208,7 @@
</ExpandingSection>
<!-- Global Discovery Settings -->
- <ExpandingSection class="glass-card !p-0 overflow-hidden">
+ <ExpandingSection class="glass-card p-0! overflow-hidden">
<template #title
><span class="text-sm font-bold">Discovery Listener (Peer)</span></template
>
@@ -1183,7 +1216,7 @@
<div class="p-6 space-y-6">
<div class="flex items-center justify-between">
<div class="max-w-md">
- <FormLabel class="glass-label !mb-0"
+ <FormLabel class="glass-label mb-0!"
>Enable Discovery Listener</FormLabel
>
<p class="text-xs text-gray-400">
@@ -1192,6 +1225,19 @@
</div>
<Toggle v-model="reticulumDiscovery.discover_interfaces" />
</div>
+ <div
+ class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between pt-2"
+ >
+ <div class="max-w-xl min-w-0">
+ <FormLabel class="glass-label mb-0! text-sm">{{
+ $t("interfaces.discovery_default_bootstrap_only")
+ }}</FormLabel>
+ <p class="text-xs text-gray-400">
+ {{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
+ </p>
+ </div>
+ <Toggle v-model="reticulumDiscovery.default_bootstrap_only" />
+ </div>
<div
v-if="reticulumDiscovery.discover_interfaces"
class="space-y-4 pt-4 border-t border-gray-100 dark:border-zinc-800 animate-in fade-in slide-in-from-top-2"
@@ -1213,7 +1259,7 @@
<div class="flex justify-end">
<button
type="button"
- class="primary-chip !text-[10px]"
+ class="primary-chip text-[10px]!"
:disabled="savingDiscovery"
@click="saveReticulumDiscoveryConfig"
>
@@ -1227,7 +1273,7 @@
</ExpandingSection>
<!-- Shared Advanced Settings -->
- <ExpandingSection class="glass-card !p-0 overflow-hidden">
+ <ExpandingSection class="glass-card p-0! overflow-hidden">
<template #title
><span class="text-sm font-bold"
>Advanced Parameters (IFAC, Mode)</span
@@ -1285,14 +1331,14 @@
>
<button
type="button"
- class="secondary-chip !px-10 !py-3 !text-sm"
+ class="secondary-chip px-10! py-3! text-sm!"
@click="$router.push({ name: 'interfaces' })"
>
Cancel
</button>
<button
type="button"
- class="primary-chip !px-16 !py-3 !text-sm"
+ class="primary-chip px-16! py-3! text-sm!"
:disabled="isSaving"
@click="saveInterface"
>
@@ -1313,7 +1359,7 @@
>
<div
v-if="!isEditingInterface && communityPresetsEnabled && communityInterfaces.length > 0"
- class="glass-card !p-0 overflow-hidden"
+ class="glass-card p-0! overflow-hidden"
>
<div
class="bg-gray-50/50 dark:bg-zinc-800/50 p-4 border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between gap-2"
@@ -1327,14 +1373,30 @@
{{ $t("interfaces.community_quick_start_hint") }}
</p>
</div>
- <button
- type="button"
- class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 transition-colors p-1 shrink-0"
- :title="$t('interfaces.community_quick_start_hide')"
- @click="updateConfig({ show_suggested_community_interfaces: false })"
- >
- <MaterialDesignIcon icon-name="close" class="size-5" />
- </button>
+ <div class="flex items-center gap-0.5 shrink-0">
+ <button
+ type="button"
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 transition-colors p-1 rounded-full"
+ :disabled="refreshingCommunityPresets"
+ :title="$t('interfaces.community_presets_refresh')"
+ :aria-label="$t('interfaces.community_presets_refresh')"
+ @click="refreshCommunityPresets"
+ >
+ <MaterialDesignIcon
+ icon-name="refresh"
+ class="size-5"
+ :class="{ 'animate-spin-reverse': refreshingCommunityPresets }"
+ />
+ </button>
+ <button
+ type="button"
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 transition-colors p-1 shrink-0"
+ :title="$t('interfaces.community_quick_start_hide')"
+ @click="updateConfig({ show_suggested_community_interfaces: false })"
+ >
+ <MaterialDesignIcon icon-name="close" class="size-5" />
+ </button>
+ </div>
</div>
<div
@@ -1383,7 +1445,7 @@
</div>
<button
type="button"
- class="primary-chip !py-1.5 !px-2 !text-[10px] shrink-0"
+ class="primary-chip py-1.5! px-2! text-[10px]! shrink-0"
@click="quickAddInterfaceFromConfig(communityIface)"
>
{{ $t("interfaces.community_use_preset") }}
@@ -1403,7 +1465,7 @@
</p>
<button
type="button"
- class="primary-chip !py-2 !px-4 !text-xs w-full"
+ class="primary-chip py-2! px-4! text-xs! w-full"
@click="updateConfig({ show_suggested_community_interfaces: true })"
>
{{ $t("interfaces.community_presets_show_again") }}
@@ -1440,14 +1502,14 @@
href="https://directory.rns.recipes/"
target="_blank"
rel="noopener noreferrer"
- class="secondary-chip !py-1 !px-2 !text-[9px]"
+ class="secondary-chip py-1! px-2! text-[9px]!"
>rns.recipes</a
>
<a
href="https://rmap.world/"
target="_blank"
rel="noopener noreferrer"
- class="secondary-chip !py-1 !px-2 !text-[9px]"
+ class="secondary-chip py-1! px-2! text-[9px]!"
>rmap.world</a
>
</div>
@@ -1455,7 +1517,7 @@
</div>
<div
- class="glass-card flex flex-col gap-2 !p-4 bg-emerald-50/20 dark:bg-emerald-900/5 border-emerald-100 dark:border-emerald-900/20"
+ class="glass-card flex flex-col gap-2 p-4! bg-emerald-50/20 dark:bg-emerald-900/5 border-emerald-100 dark:border-emerald-900/20"
>
<div class="flex items-center justify-between gap-2">
<h3
@@ -1471,7 +1533,7 @@
<textarea
v-model="rawConfigInput"
:placeholder="$t('interfaces.quick_import_placeholder')"
- class="w-full h-20 bg-white/50 dark:bg-zinc-900/50 border border-emerald-100/50 dark:border-emerald-900/30 rounded-xl p-2 text-[10px] font-mono focus:ring-1 focus:ring-emerald-500 outline-none transition"
+ class="w-full h-20 bg-white/50 dark:bg-zinc-900/50 border border-emerald-100/50 dark:border-emerald-900/30 rounded-xl p-2 text-[10px] font-mono focus:ring-1 focus:ring-emerald-500 outline-hidden transition"
@input="handleRawConfigInput"
></textarea>
@@ -1551,6 +1613,7 @@ export default {
newInterfaceConnectTimeout: null,
newInterfaceMaxReconnectTries: null,
newInterfaceFixedMTU: null,
+ newInterfaceBootstrapOnly: true,
newInterfaceConfiguredBitrate: null,
newInterfaceConnectable: true,
newInterfaceBackboneListenMode: false,
@@ -1589,10 +1652,12 @@ export default {
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
+ default_bootstrap_only: true,
network_identity: "",
},
savingDiscovery: false,
+ refreshingCommunityPresets: false,
newInterfaceForwardIp: null,
newInterfaceForwardPort: null,
@@ -1774,6 +1839,12 @@ export default {
this.reticulumDiscovery.discover_interfaces = this.parseBool(discovery.discover_interfaces);
this.reticulumDiscovery.interface_discovery_whitelist = discovery.interface_discovery_whitelist ?? "";
this.reticulumDiscovery.interface_discovery_blacklist = discovery.interface_discovery_blacklist ?? "";
+ this.reticulumDiscovery.default_bootstrap_only = this.parseBool(
+ discovery.default_bootstrap_only ?? true
+ );
+ if (!this.isEditingInterface) {
+ this.newInterfaceBootstrapOnly = this.reticulumDiscovery.default_bootstrap_only;
+ }
} catch (e) {
console.log(e);
}
@@ -1786,6 +1857,7 @@ export default {
discover_interfaces: this.reticulumDiscovery.discover_interfaces,
interface_discovery_whitelist: this.reticulumDiscovery.interface_discovery_whitelist || null,
interface_discovery_blacklist: this.reticulumDiscovery.interface_discovery_blacklist || null,
+ default_bootstrap_only: this.reticulumDiscovery.default_bootstrap_only,
};
await window.api.patch(`/api/v1/reticulum/discovery`, payload);
ToastUtils.success("Discovery listener preferences saved.");
@@ -1815,6 +1887,22 @@ export default {
this.communityInterfacesFetchDone = true;
}
},
+ async refreshCommunityPresets() {
+ if (this.refreshingCommunityPresets) return;
+ this.refreshingCommunityPresets = true;
+ try {
+ const r = await window.api.post("/api/v1/community-interfaces/refresh", {});
+ const n = r.data?.count ?? 0;
+ ToastUtils.success(this.$t("interfaces.community_presets_refreshed", { count: n }));
+ await this.loadCommunityInterfaces();
+ } catch (e) {
+ const msg = e.response?.data?.message || this.$t("interfaces.community_presets_refresh_failed");
+ ToastUtils.error(msg);
+ console.log(e);
+ } finally {
+ this.refreshingCommunityPresets = false;
+ }
+ },
async loadInterfaceToEdit(interfaceName) {
try {
const response = await window.api.get(`/api/v1/reticulum/interfaces`);
@@ -1855,6 +1943,18 @@ export default {
this.newInterfaceBackboneListenDevice = iface.device ?? null;
}
+ if (
+ iface.type === "TCPClientInterface" ||
+ (iface.type === "BackboneInterface" && !(iface.listen_port != null && iface.listen_port !== ""))
+ ) {
+ this.newInterfaceBootstrapOnly =
+ iface.bootstrap_only !== undefined &&
+ iface.bootstrap_only !== null &&
+ iface.bootstrap_only !== ""
+ ? this.parseBool(iface.bootstrap_only)
+ : false;
+ }
+
this.newInterfaceGroupID = iface.group_id ?? null;
this.newInterfaceMulticastAddressType = iface.multicast_address_type ?? null;
this.newInterfaceDevices = iface.devices ?? null;
@@ -2059,6 +2159,9 @@ export default {
if (config.connect_timeout) this.newInterfaceConnectTimeout = Number(config.connect_timeout);
if (config.max_reconnect_tries) this.newInterfaceMaxReconnectTries = Number(config.max_reconnect_tries);
if (config.fixed_mtu) this.newInterfaceFixedMTU = Number(config.fixed_mtu);
+ if (config.bootstrap_only !== undefined && config.bootstrap_only !== null && config.bootstrap_only !== "") {
+ this.newInterfaceBootstrapOnly = this.parseBool(config.bootstrap_only);
+ }
if (config.device) this.newInterfaceNetworkDevice = config.device;
if (config.prefer_ipv6 !== undefined) this.newInterfacePreferIPV6 = this.parseBool(config.prefer_ipv6);
if (config.connectable !== undefined) this.newInterfaceConnectable = this.parseBool(config.connectable);
@@ -2099,6 +2202,20 @@ export default {
config.discoverable !== undefined && config.discoverable !== null && config.discoverable !== ""
? this.parseBool(config.discoverable)
: false;
+ const backboneConnector =
+ config.type === "BackboneInterface" &&
+ Boolean(config.remote || config.target_host) &&
+ !(config.listen_port != null && String(config.listen_port).trim() !== "");
+ let bootstrapOnlyPayload;
+ if (config.type === "TCPClientInterface" || backboneConnector) {
+ if (
+ config.bootstrap_only !== undefined &&
+ config.bootstrap_only !== null &&
+ config.bootstrap_only !== ""
+ ) {
+ bootstrapOnlyPayload = this.parseBool(config.bootstrap_only);
+ }
+ }
const i2pPeers =
config.type === "I2PInterface"
? Array.isArray(config.i2p_peers)
@@ -2200,6 +2317,7 @@ export default {
config.flow_control !== undefined && config.flow_control !== null && config.flow_control !== ""
? this.parseBool(config.flow_control)
: null,
+ bootstrap_only: bootstrapOnlyPayload,
};
},
applyDiscoveredInterfacePrefill() {
@@ -2306,6 +2424,11 @@ export default {
connect_timeout: this.numOrNull(this.newInterfaceConnectTimeout),
max_reconnect_tries: this.numOrNull(this.newInterfaceMaxReconnectTries),
fixed_mtu: this.numOrNull(this.newInterfaceFixedMTU),
+ bootstrap_only:
+ this.newInterfaceType === "TCPClientInterface" ||
+ (this.newInterfaceType === "BackboneInterface" && !this.newInterfaceBackboneListenMode)
+ ? this.newInterfaceBootstrapOnly === true
+ : undefined,
connectable:
this.newInterfaceType === "I2PInterface" ? this.newInterfaceConnectable === true : null,
port: this.newInterfaceRNodeUseIP
@@ -2437,8 +2560,9 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.glass-card {
- @apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-xl p-6;
+ @apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-xl p-6;
}
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition;
diff --git a/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue b/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue
index d73ee2d6..fe76119e 100644
--- a/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue
+++ b/meshchatx/src/frontend/components/interfaces/ExpandingSection.vue
@@ -1,7 +1,9 @@
<!-- SPDX-License-Identifier: 0BSD AND MIT -->
<template>
- <div class="bg-white rounded shadow divide-y divide-gray-300 dark:divide-zinc-700 dark:bg-zinc-900 overflow-hidden">
+ <div
+ class="bg-white rounded-sm shadow-sm divide-y divide-gray-300 dark:divide-zinc-700 dark:bg-zinc-900 overflow-hidden"
+ >
<div
class="flex p-2 justify-between cursor-pointer hover:bg-gray-50 dark:hover:bg-zinc-800"
@click="isExpanded = !isExpanded"
diff --git a/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue b/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue
index b5f4bb4b..56549208 100644
--- a/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue
+++ b/meshchatx/src/frontend/components/interfaces/ImportInterfacesModal.vue
@@ -55,7 +55,7 @@
<div
v-for="iface in importableInterfaces"
:key="iface.name"
- class="bg-white cursor-pointer flex items-center p-2 border rounded shadow dark:bg-zinc-900 dark:border-zinc-700"
+ class="bg-white cursor-pointer flex items-center p-2 border rounded-sm shadow-sm dark:bg-zinc-900 dark:border-zinc-700"
>
<div class="mr-auto text-sm flex-1" @click="toggleSelectedInterface(iface.name)">
<div class="font-semibold text-gray-700 dark:text-zinc-100">{{ iface.name }}</div>
diff --git a/meshchatx/src/frontend/components/interfaces/Interface.vue b/meshchatx/src/frontend/components/interfaces/Interface.vue
index 930336be..14c14179 100644
--- a/meshchatx/src/frontend/components/interfaces/Interface.vue
+++ b/meshchatx/src/frontend/components/interfaces/Interface.vue
@@ -49,7 +49,7 @@
}}</span>
<span v-if="isDiscoverable()" class="discoverable-chip shrink-0">Discoverable</span>
</div>
- <div class="text-sm text-gray-600 dark:text-gray-300 break-words min-w-0">
+ <div class="text-sm text-gray-600 dark:text-gray-300 wrap-break-word min-w-0">
{{ description }}
</div>
<div class="flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
@@ -303,8 +303,9 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.interface-card {
- @apply relative bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg p-4 space-y-3 hover:z-10 min-w-0;
+ @apply relative bg-white/95 dark:bg-zinc-900/85 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg p-4 space-y-3 hover:z-10 min-w-0;
overflow: visible;
}
.interface-card__icon {
diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
index cd6be554..f8ccb482 100644
--- a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
+++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
@@ -2,10 +2,10 @@
<template>
<div
- class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
+ class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-4 sm:py-6">
- <div class="space-y-0 w-full min-w-0 max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto flex-1">
+ <div class="space-y-0 w-full min-w-0 max-w-6xl xl:max-w-7xl 2xl:max-w-360 mx-auto flex-1">
<div
v-if="showRestartReminder"
class="bg-amber-600 text-white border border-amber-500/30 p-4 sm:rounded-xl flex flex-wrap gap-3 items-center mb-4 sm:mb-6"
@@ -20,7 +20,7 @@
<button
v-if="isElectron"
type="button"
- class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-sm"
+ class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-xs"
@click="relaunch"
>
<MaterialDesignIcon icon-name="restart" class="w-4 h-4" />
@@ -71,6 +71,20 @@
<MaterialDesignIcon icon-name="restart" class="w-4 h-4" />
<span>{{ reloadingRns ? $t("app.reloading_rns") : "Restart RNS" }}</span>
</button>
+ <button
+ type="button"
+ class="secondary-chip text-sm min-h-[44px] sm:min-h-0 inline-flex items-center justify-center gap-1.5"
+ :disabled="refreshingCommunityInterfaces"
+ :title="$t('interfaces.community_presets_refresh')"
+ :aria-label="$t('interfaces.community_presets_refresh')"
+ @click="refreshCommunityInterfaces"
+ >
+ <MaterialDesignIcon
+ icon-name="refresh"
+ class="w-4 h-4"
+ :class="{ 'animate-spin-reverse': refreshingCommunityInterfaces }"
+ />
+ </button>
</div>
</div>
@@ -84,7 +98,7 @@
v-model="searchTerm"
type="text"
:placeholder="$t('interfaces.search_placeholder')"
- class="w-full pl-12 pr-4 py-3 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 shadow-sm"
+ class="w-full pl-12 pr-4 py-3 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl focus:outline-hidden focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 shadow-xs"
/>
<button
v-if="searchTerm"
@@ -97,7 +111,7 @@
<div>
<select
v-model="typeFilter"
- class="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-white"
+ class="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-white"
>
<option value="all">{{ $t("interfaces.all_types") }}</option>
<option v-for="type in sortedInterfaceTypes" :key="type" :value="type">
@@ -142,7 +156,7 @@
<button
type="button"
:class="filterChipClass(statusFilter === 'all')"
- class="!py-1 !px-3"
+ class="py-1! px-3!"
@click="setStatusFilter('all')"
>
{{ $t("interfaces.all") }}
@@ -150,7 +164,7 @@
<button
type="button"
:class="filterChipClass(statusFilter === 'enabled')"
- class="!py-1 !px-3"
+ class="py-1! px-3!"
@click="setStatusFilter('enabled')"
>
{{ $t("app.enabled") }}
@@ -158,7 +172,7 @@
<button
type="button"
:class="filterChipClass(statusFilter === 'disabled')"
- class="!py-1 !px-3"
+ class="py-1! px-3!"
@click="setStatusFilter('disabled')"
>
{{ $t("app.disabled") }}
@@ -215,7 +229,7 @@
<button
type="button"
:class="filterChipClass(discoveredStatusFilter === 'all')"
- class="!py-1 !px-3"
+ class="py-1! px-3!"
@click="discoveredStatusFilter = 'all'"
>
{{ $t("interfaces.all") }}
@@ -223,7 +237,7 @@
<button
type="button"
:class="filterChipClass(discoveredStatusFilter === 'connected')"
- class="!py-1 !px-3"
+ class="py-1! px-3!"
@click="discoveredStatusFilter = 'connected'"
>
{{ $t("interfaces.connected_only") }}
@@ -465,7 +479,7 @@
<div class="relative">
<button
type="button"
- class="secondary-chip !p-2 !rounded-xl"
+ class="secondary-chip p-2! rounded-xl!"
title="Discovery actions"
@click="toggleDiscoveryActionsMenu(iface)"
>
@@ -513,7 +527,7 @@
<button
v-if="iface.latitude != null && iface.longitude != null"
type="button"
- class="secondary-chip !p-2 !rounded-xl"
+ class="secondary-chip p-2! rounded-xl!"
:title="$t('map.title')"
@click="goToMap(iface)"
>
@@ -633,6 +647,24 @@
0 disables auto-connect.
</div>
</div>
+ <div class="sm:col-span-2">
+ <div
+ class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
+ >
+ <div class="min-w-0 pr-0 sm:pr-4">
+ <div class="text-sm font-semibold text-gray-900 dark:text-white">
+ {{ $t("interfaces.discovery_default_bootstrap_only") }}
+ </div>
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
+ </div>
+ </div>
+ <Toggle
+ v-model="discoveryConfig.default_bootstrap_only"
+ class="shrink-0 sm:my-auto"
+ />
+ </div>
+ </div>
<div>
<div class="text-xs font-semibold text-gray-700 dark:text-gray-200">
Network Identity Path
@@ -671,7 +703,7 @@
<RouterLink
:to="{ name: 'interfaces.add' }"
- class="sm:hidden fixed bottom-5 right-4 z-[60] flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
+ class="sm:hidden fixed bottom-5 right-4 z-60 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
:title="$t('interfaces.add_interface')"
>
<MaterialDesignIcon icon-name="plus" class="w-7 h-7" />
@@ -717,6 +749,7 @@ export default {
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
+ default_bootstrap_only: true,
network_identity: "",
},
savingDiscovery: false,
@@ -727,6 +760,7 @@ export default {
discoveredStatusFilter: "all",
discoveryInterval: null,
activeTab: "overview",
+ refreshingCommunityInterfaces: false,
};
},
computed: {
@@ -1175,6 +1209,7 @@ export default {
discovery.autoconnect_discovered_interfaces !== ""
? Number(discovery.autoconnect_discovered_interfaces)
: 0;
+ this.discoveryConfig.default_bootstrap_only = this.parseBool(discovery.default_bootstrap_only ?? true);
this.discoveryConfig.network_identity = discovery.network_identity ?? "";
} catch (e) {
console.log(e);
@@ -1199,6 +1234,7 @@ export default {
this.discoveryConfig.autoconnect_discovered_interfaces === ""
? 0
: Number(this.discoveryConfig.autoconnect_discovered_interfaces),
+ default_bootstrap_only: this.discoveryConfig.default_bootstrap_only,
network_identity: this.discoveryConfig.network_identity || null,
};
@@ -1438,6 +1474,21 @@ export default {
filterChipClass(isActive) {
return isActive ? "primary-chip text-xs" : "secondary-chip text-xs";
},
+ async refreshCommunityInterfaces() {
+ if (this.refreshingCommunityInterfaces) return;
+ this.refreshingCommunityInterfaces = true;
+ try {
+ const r = await window.api.post("/api/v1/community-interfaces/refresh", {});
+ const n = r.data?.count ?? 0;
+ ToastUtils.success(this.$t("interfaces.community_presets_refreshed", { count: n }));
+ } catch (e) {
+ const msg = e.response?.data?.message || this.$t("interfaces.community_presets_refresh_failed");
+ ToastUtils.error(msg);
+ console.error(e);
+ } finally {
+ this.refreshingCommunityInterfaces = false;
+ }
+ },
async reloadRns() {
if (this.reloadingRns) return;
@@ -1462,6 +1513,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.interfaces-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8;
}
diff --git a/meshchatx/src/frontend/components/layout/AppShellBanners.vue b/meshchatx/src/frontend/components/layout/AppShellBanners.vue
index 924c22ea..be8d95cf 100644
--- a/meshchatx/src/frontend/components/layout/AppShellBanners.vue
+++ b/meshchatx/src/frontend/components/layout/AppShellBanners.vue
@@ -4,7 +4,7 @@
<div>
<div
v-if="showEmergency"
- class="relative z-[100] bg-red-600 text-white px-4 py-2 text-center text-sm font-bold shadow-md animate-pulse"
+ class="relative z-100 bg-red-600 text-white px-4 py-2 text-center text-sm font-bold shadow-md animate-pulse"
>
<div class="flex items-center justify-center gap-2">
<MaterialDesignIcon icon-name="alert-decagram" class="size-5" />
@@ -14,7 +14,7 @@
<div
v-if="showWsDisconnected"
- class="relative z-[100] bg-red-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-red-800/80"
+ class="relative z-100 bg-red-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-red-800/80"
role="status"
aria-live="polite"
>
@@ -22,7 +22,7 @@
</div>
<div
v-if="showWsReconnected"
- class="relative z-[100] bg-emerald-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-emerald-800/80 transition-opacity duration-300"
+ class="relative z-100 bg-emerald-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-emerald-800/80 transition-opacity duration-300"
role="status"
aria-live="polite"
>
diff --git a/meshchatx/src/frontend/components/licenses/LicensesPage.vue b/meshchatx/src/frontend/components/licenses/LicensesPage.vue
index 6d828924..48d28912 100644
--- a/meshchatx/src/frontend/components/licenses/LicensesPage.vue
+++ b/meshchatx/src/frontend/components/licenses/LicensesPage.vue
@@ -17,7 +17,7 @@
<div class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed max-w-2xl">
{{ $t("licenses.description") }}
</div>
- <p v-if="meta?.generated_at" class="text-xs text-gray-500 dark:text-zinc-500 break-words">
+ <p v-if="meta?.generated_at" class="text-xs text-gray-500 dark:text-zinc-500 wrap-break-word">
{{ $t("licenses.generated_at", { time: meta.generated_at }) }}
<span v-if="meta.frontend_source" class="ml-2 inline-block sm:inline">
({{ $t("licenses.frontend_source", { source: meta.frontend_source }) }})
@@ -39,7 +39,7 @@
enterkeyhint="search"
autocomplete="off"
:placeholder="$t('licenses.search_placeholder')"
- class="w-full min-h-[44px] sm:min-h-0 pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-base sm:text-sm"
+ class="w-full min-h-[44px] sm:min-h-0 pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-hidden focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-base sm:text-sm"
/>
<button
v-if="searchQuery"
@@ -82,7 +82,7 @@
<summary
class="cursor-pointer select-none px-3 sm:px-4 py-3.5 sm:py-3 min-h-[48px] sm:min-h-0 font-semibold text-sm sm:text-base text-gray-900 dark:text-white flex items-center justify-between gap-2 list-none touch-manipulation"
>
- <span class="min-w-0 break-words pr-2"
+ <span class="min-w-0 wrap-break-word pr-2"
>{{ $t("licenses.backend_section") }} ({{ filteredBackend.length }})</span
>
<MaterialDesignIcon
@@ -96,7 +96,7 @@
<table class="min-w-full text-left border-collapse text-xs sm:text-sm">
<thead>
<tr
- class="sticky top-0 z-[1] border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-sm text-gray-600 dark:text-zinc-400"
+ class="sticky top-0 z-1 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-xs text-gray-600 dark:text-zinc-400"
>
<th class="py-2 px-2 sm:px-3 font-medium">
{{ $t("licenses.col_package") }}
@@ -129,13 +129,13 @@
{{ row.version }}
</td>
<td
- class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[10rem] sm:max-w-[14rem] truncate align-top"
+ class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-40 sm:max-w-56 truncate align-top"
:title="row.author"
>
{{ row.author }}
</td>
<td
- class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[8rem] sm:max-w-xs align-top break-words"
+ class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-32 sm:max-w-xs align-top wrap-break-word"
>
{{ row.license }}
</td>
@@ -158,7 +158,7 @@
<summary
class="cursor-pointer select-none px-3 sm:px-4 py-3.5 sm:py-3 min-h-[48px] sm:min-h-0 font-semibold text-sm sm:text-base text-gray-900 dark:text-white flex items-center justify-between gap-2 list-none touch-manipulation"
>
- <span class="min-w-0 break-words pr-2"
+ <span class="min-w-0 wrap-break-word pr-2"
>{{ $t("licenses.frontend_section") }} ({{ filteredFrontend.length }})</span
>
<MaterialDesignIcon
@@ -172,7 +172,7 @@
<table class="min-w-full text-left border-collapse text-xs sm:text-sm">
<thead>
<tr
- class="sticky top-0 z-[1] border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-sm text-gray-600 dark:text-zinc-400"
+ class="sticky top-0 z-1 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-xs text-gray-600 dark:text-zinc-400"
>
<th class="py-2 px-2 sm:px-3 font-medium">
{{ $t("licenses.col_package") }}
@@ -205,13 +205,13 @@
{{ row.version }}
</td>
<td
- class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[10rem] sm:max-w-[14rem] truncate align-top"
+ class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-40 sm:max-w-56 truncate align-top"
:title="row.author"
>
{{ row.author }}
</td>
<td
- class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[8rem] sm:max-w-xs align-top break-words"
+ class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-32 sm:max-w-xs align-top wrap-break-word"
>
{{ row.license }}
</td>
diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue
index 5b01907b..ed47f223 100644
--- a/meshchatx/src/frontend/components/map/MapPage.vue
+++ b/meshchatx/src/frontend/components/map/MapPage.vue
@@ -4,7 +4,7 @@
<div class="flex flex-col h-full w-full bg-white dark:bg-zinc-950 overflow-hidden">
<!-- header -->
<div
- class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-0 px-3 py-2 sm:px-4 border-b border-gray-200 dark:border-zinc-800 bg-white/80 dark:bg-zinc-900/80 backdrop-blur z-10 relative"
+ class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-0 px-3 py-2 sm:px-4 border-b border-gray-200 dark:border-zinc-800 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm z-10 relative"
>
<div class="hidden sm:flex items-center min-w-0 gap-2">
<v-icon icon="mdi-map" class="text-blue-500 dark:text-blue-400 shrink-0" size="24"></v-icon>
@@ -21,7 +21,7 @@
<button
:class="
discoveredVisible
- ? 'bg-white dark:bg-zinc-700 shadow-sm text-emerald-600 dark:text-emerald-400'
+ ? 'bg-white dark:bg-zinc-700 shadow-xs text-emerald-600 dark:text-emerald-400'
: 'text-gray-500 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-zinc-700'
"
class="p-1.5 sm:p-2 rounded-lg transition-colors shrink-0"
@@ -34,7 +34,7 @@
<button
:class="
!offlineEnabled
- ? 'bg-white dark:bg-zinc-700 shadow-sm text-blue-600 dark:text-blue-400'
+ ? 'bg-white dark:bg-zinc-700 shadow-xs text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-300'
"
class="px-2 py-1 text-xs sm:px-3 sm:text-sm font-medium rounded-md transition-all shrink-0"
@@ -45,7 +45,7 @@
<button
:class="
offlineEnabled
- ? 'bg-white dark:bg-zinc-700 shadow-sm text-blue-600 dark:text-blue-400'
+ ? 'bg-white dark:bg-zinc-700 shadow-xs text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-300'
"
class="px-2 py-1 text-xs sm:px-3 sm:text-sm font-medium rounded-md transition-all shrink-0"
@@ -59,7 +59,7 @@
<!-- upload: icon on mobile, full label from sm -->
<button
type="button"
- class="inline-flex items-center justify-center sm:gap-1 p-2 sm:px-3 sm:py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow-sm transition-colors text-sm font-medium shrink-0"
+ class="inline-flex items-center justify-center sm:gap-1 p-2 sm:px-3 sm:py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow-xs transition-colors text-sm font-medium shrink-0"
:title="$t('map.upload_mbtiles')"
@click="$refs.fileInput.click()"
>
@@ -160,7 +160,7 @@
(hoveredFeature.get('telemetry') && hoveredFeature.get('telemetry').note)) &&
!editingFeature
"
- class="absolute pointer-events-none z-50 bg-white/90 dark:bg-zinc-900/90 backdrop-blur border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xl p-2 text-sm text-gray-900 dark:text-zinc-100 max-w-xs transform -translate-x-1/2 -translate-y-full mb-4"
+ class="absolute pointer-events-none z-50 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-sm border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xl p-2 text-sm text-gray-900 dark:text-zinc-100 max-w-xs transform -translate-x-1/2 -translate-y-full mb-4"
:style="{
left: map.getPixelFromCoordinate(hoveredFeature.getGeometry().getCoordinates())[0] + 'px',
top: map.getPixelFromCoordinate(hoveredFeature.getGeometry().getCoordinates())[1] + 'px',
@@ -172,7 +172,7 @@
hoveredFeature.get("telemetry") ? hoveredFeature.get("peer")?.display_name || "Peer" : "Note"
}}</span>
</div>
- <div class="whitespace-pre-wrap break-words">
+ <div class="whitespace-pre-wrap wrap-break-word">
{{ hoveredFeature.get("note") || hoveredFeature.get("telemetry")?.note }}
</div>
</div>
@@ -197,7 +197,7 @@
</div>
<textarea
v-model="noteText"
- class="w-full h-24 p-2 text-sm bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none text-gray-900 dark:text-zinc-100"
+ class="w-full h-24 p-2 text-sm bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-hidden resize-none text-gray-900 dark:text-zinc-100"
placeholder="Type your note here..."
></textarea>
<div class="flex justify-between mt-3">
@@ -209,7 +209,7 @@
Delete
</button>
<button
- class="px-3 py-1.5 text-xs font-semibold bg-amber-500 text-white hover:bg-amber-600 rounded-lg shadow-sm transition-colors"
+ class="px-3 py-1.5 text-xs font-semibold bg-amber-500 text-white hover:bg-amber-600 rounded-lg shadow-xs transition-colors"
@click="saveNote"
>
Save
@@ -218,17 +218,17 @@
</div>
</div>
- <div ref="drawFeatureInfoElement" class="absolute z-[45] pointer-events-none">
+ <div ref="drawFeatureInfoElement" class="absolute z-45 pointer-events-none">
<div
v-show="drawFeatureInfoPayload"
- class="pointer-events-auto min-w-[11rem] max-w-[min(18rem,calc(100vw-2rem))] rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900/95 backdrop-blur shadow-xl px-3 py-2.5 transform -translate-x-1/2 -translate-y-full mb-2"
+ class="pointer-events-auto min-w-44 max-w-[min(18rem,calc(100vw-2rem))] rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-xl px-3 py-2.5 transform -translate-x-1/2 -translate-y-full mb-2"
>
<template v-if="drawFeatureInfoPayload">
<div v-if="drawFeatureInfoPayload.iconSrc" class="flex justify-center mb-2">
<img
:src="drawFeatureInfoPayload.iconSrc"
alt=""
- class="max-h-12 max-w-[4.5rem] object-contain rounded border border-gray-100 dark:border-zinc-800 bg-gray-50 dark:bg-zinc-800/50"
+ class="max-h-12 max-w-18 object-contain rounded-sm border border-gray-100 dark:border-zinc-800 bg-gray-50 dark:bg-zinc-800/50"
/>
</div>
<div
@@ -239,7 +239,7 @@
</div>
<div
v-if="drawFeatureInfoPayload.description && !drawFeatureInfoPayload.descriptionIsHtml"
- class="text-[11px] text-gray-600 dark:text-zinc-400 whitespace-pre-wrap break-words leading-snug"
+ class="text-[11px] text-gray-600 dark:text-zinc-400 whitespace-pre-wrap wrap-break-word leading-snug"
>
{{ drawFeatureInfoPayload.description }}
</div>
@@ -262,7 +262,9 @@
>
{{ row.key }}
</dt>
- <dd class="text-gray-800 dark:text-zinc-200 break-words m-0">{{ row.value }}</dd>
+ <dd class="text-gray-800 dark:text-zinc-200 wrap-break-word m-0">
+ {{ row.value }}
+ </dd>
</div>
</template>
</dl>
@@ -275,7 +277,7 @@
:show="showContextMenu"
:x="contextMenuPos.x"
:y="contextMenuPos.y"
- panel-class="z-[120] overflow-hidden text-sm"
+ panel-class="z-120 overflow-hidden text-sm"
>
<template #header>
<div
@@ -369,7 +371,7 @@
<div
v-if="showMapPingModal"
- class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 p-4"
+ class="fixed inset-0 z-200 flex items-center justify-center bg-black/40 p-4"
role="dialog"
aria-modal="true"
@click.self="showMapPingModal = false"
@@ -424,7 +426,7 @@
<div
v-show="!isMobileScreen"
ref="scaleLineMount"
- class="ol-scale-line-host absolute z-10 bottom-4 right-4 sm:bottom-4 max-sm:bottom-[5.5rem] pointer-events-auto min-w-[120px] max-w-[min(55vw,14rem)]"
+ class="ol-scale-line-host absolute z-10 bottom-4 right-4 sm:bottom-4 max-sm:bottom-22 pointer-events-auto min-w-[120px] max-w-[min(55vw,14rem)]"
:class="{ 'ol-scale-line-host--dark-basemap': isDarkRasterBasemap }"
></div>
@@ -433,7 +435,7 @@
class="absolute bottom-4 left-4 z-10 flex flex-col gap-2 pointer-events-none max-w-[min(100vw-2rem,22rem)]"
>
<div
- class="flex flex-col items-center justify-end text-gray-800 dark:text-zinc-100 bg-white/80 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1 shadow-sm pointer-events-auto w-fit"
+ class="flex flex-col items-center justify-end text-gray-800 dark:text-zinc-100 bg-white/80 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1 shadow-xs pointer-events-auto w-fit"
:title="$t('map.north_up')"
>
<div
@@ -449,7 +451,7 @@
</div>
<div
v-if="metadata && metadata.name && !metadata.name.startsWith('Map Export')"
- class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-xs text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-sm"
+ class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-xs text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-xs"
>
<div class="font-semibold text-gray-900 dark:text-zinc-100 mb-1">
{{ metadata.name }}
@@ -465,7 +467,7 @@
<!-- Lat/Lon Box -->
<div
- class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-[10px] font-mono text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-sm flex flex-col space-y-0.5"
+ class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-[10px] font-mono text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-xs flex flex-col space-y-0.5"
>
<div class="flex justify-between space-x-4">
<span class="opacity-50 uppercase tracking-tighter">Lat</span>
@@ -482,7 +484,7 @@
<div
v-if="isSettingsOpen"
ref="settingsPanel"
- class="absolute z-20 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden flex flex-col min-h-0 animate-in fade-in zoom-in-95 duration-200"
+ class="absolute z-20 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xs rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden flex flex-col min-h-0 animate-in fade-in zoom-in-95 duration-200"
:class="
settingsPanelPos
? 'w-96 max-w-[min(100vw-2rem,28rem)] max-h-full'
@@ -524,7 +526,7 @@
<!-- Quick Actions -->
<div class="grid grid-cols-2 gap-2">
<button
- class="flex items-center justify-center space-x-1.5 px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95"
+ class="flex items-center justify-center space-x-1.5 px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95"
@click="setAsDefaultView"
>
<MaterialDesignIcon icon-name="pin" class="size-3" />
@@ -578,7 +580,7 @@
(style.id === 'carto-voyager' && tileServerUrl.includes('rastertiles/voyager')) ||
(style.id === 'carto-light' &&
tileServerUrl.includes('basemaps.cartocdn.com/light_all'))
- ? 'bg-blue-500 border-blue-600 text-white shadow-sm ring-2 ring-blue-500/20'
+ ? 'bg-blue-500 border-blue-600 text-white shadow-xs ring-2 ring-blue-500/20'
: 'bg-white dark:bg-zinc-900 border-gray-200 dark:border-zinc-800 text-gray-500 dark:text-zinc-400 hover:bg-gray-50 dark:hover:bg-zinc-800'
"
@click="setTileServer(style.id)"
@@ -597,7 +599,7 @@
<input
v-model="tileServerUrl"
type="text"
- class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-none"
+ class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-hidden"
:placeholder="$t('map.tile_server_url_placeholder')"
@blur="saveTileServerUrl"
/>
@@ -613,7 +615,7 @@
<input
v-model="nominatimApiUrl"
type="text"
- class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-none"
+ class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-hidden"
:placeholder="$t('map.nominatim_api_url_placeholder')"
@blur="saveNominatimApiUrl"
/>
@@ -688,7 +690,7 @@
<input
v-model="mbtilesDir"
type="text"
- class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-none"
+ class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-hidden"
placeholder="Default storage"
@blur="saveMBTilesDir"
/>
@@ -710,7 +712,7 @@
:class="
file.is_active
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50'
- : 'bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 shadow-sm'
+ : 'bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 shadow-xs'
"
>
<div class="flex flex-col min-w-0 flex-1 mr-2">
@@ -845,7 +847,7 @@
<!-- onboarding tooltip -->
<div
v-if="showOnboardingTooltip"
- class="fixed inset-0 z-[100] pointer-events-none"
+ class="fixed inset-0 z-100 pointer-events-none"
@click="dismissOnboardingTooltip"
>
<div class="absolute inset-0 bg-black/50 pointer-events-auto"></div>
@@ -897,7 +899,7 @@
<!-- save drawing modal -->
<div
v-if="showSaveDrawingModal"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
>
<div
class="bg-white dark:bg-zinc-900 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200"
@@ -949,7 +951,7 @@
<!-- load drawing modal -->
<div
v-if="showLoadDrawingModal"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
>
<div
class="bg-white dark:bg-zinc-900 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200"
@@ -1023,7 +1025,7 @@
<transition name="fade">
<div
v-if="showNoteModal"
- class="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-end sm:items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="closeNoteEditor"
>
<div
@@ -1044,7 +1046,7 @@
<div class="p-4">
<textarea
v-model="noteText"
- class="w-full h-40 p-4 text-base bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none text-gray-900 dark:text-zinc-100"
+ class="w-full h-40 p-4 text-base bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-hidden resize-none text-gray-900 dark:text-zinc-100"
placeholder="Type your note here..."
autofocus
></textarea>
@@ -1058,7 +1060,7 @@
Delete
</button>
<button
- class="flex-[2] px-4 py-3 text-sm font-bold bg-amber-500 text-white hover:bg-amber-600 rounded-xl shadow-lg shadow-amber-500/30 transition-colors"
+ class="flex-2 px-4 py-3 text-sm font-bold bg-amber-500 text-white hover:bg-amber-600 rounded-xl shadow-lg shadow-amber-500/30 transition-colors"
@click="saveNote"
>
Save Note
diff --git a/meshchatx/src/frontend/components/map/MiniChat.vue b/meshchatx/src/frontend/components/map/MiniChat.vue
index 403020d2..377f597d 100644
--- a/meshchatx/src/frontend/components/map/MiniChat.vue
+++ b/meshchatx/src/frontend/components/map/MiniChat.vue
@@ -17,7 +17,7 @@
:class="msg.is_outbound ? 'ml-auto items-end' : 'mr-auto items-start'"
>
<div
- class="px-2 py-1 rounded-lg text-xs break-words shadow-sm"
+ class="px-2 py-1 rounded-lg text-xs wrap-break-word shadow-xs"
:class="
msg.is_outbound
? 'bg-blue-600 text-white'
@@ -81,7 +81,7 @@
<input
v-model="newMessage"
type="text"
- class="flex-1 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-md px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-zinc-100"
+ class="flex-1 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-zinc-100"
placeholder="Type a message..."
@keydown.enter="sendMessage"
/>
diff --git a/meshchatx/src/frontend/components/map/internal/MapBearingInstructions.vue b/meshchatx/src/frontend/components/map/internal/MapBearingInstructions.vue
index b6e08502..95546a15 100644
--- a/meshchatx/src/frontend/components/map/internal/MapBearingInstructions.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapBearingInstructions.vue
@@ -2,10 +2,10 @@
<template>
<div
- class="absolute top-[calc(0.5rem+2.75rem+0.5rem+2.75rem)] left-1/2 -translate-x-1/2 z-[19] w-[min(100vw-2rem,24rem)] pointer-events-auto"
+ class="absolute top-[calc(0.5rem+2.75rem+0.5rem+2.75rem)] left-1/2 -translate-x-1/2 z-19 w-[min(100vw-2rem,24rem)] pointer-events-auto"
>
<div
- class="bg-white/95 dark:bg-zinc-900/95 backdrop-blur border border-gray-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-2 text-xs text-gray-800 dark:text-zinc-200"
+ class="bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm border border-gray-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-2 text-xs text-gray-800 dark:text-zinc-200"
>
<p class="font-medium text-center" :class="showFromHere ? 'mb-2' : ''">{{ instructionText }}</p>
<button
diff --git a/meshchatx/src/frontend/components/map/internal/MapDrawingToolbar.vue b/meshchatx/src/frontend/components/map/internal/MapDrawingToolbar.vue
index 604de182..220731ea 100644
--- a/meshchatx/src/frontend/components/map/internal/MapDrawingToolbar.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapDrawingToolbar.vue
@@ -20,7 +20,7 @@
:title="tool.type === 'Export' ? 'MBTiles exporter' : $t(`map.tool_${tool.type.toLowerCase()}`)"
@click="onToolClick(tool)"
>
- <v-icon :icon="'mdi-' + tool.icon" size="18" class="sm:!size-5"></v-icon>
+ <v-icon :icon="'mdi-' + tool.icon" size="18" class="sm:size-5!"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-0.5 sm:mx-1"></div>
<button
@@ -33,7 +33,7 @@
:title="$t('map.tool_measure')"
@click="$emit('toggle-measure')"
>
- <v-icon icon="mdi-ruler" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-ruler" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
@@ -45,7 +45,7 @@
:title="$t('map.tool_bearing')"
@click="$emit('toggle-bearing')"
>
- <v-icon icon="mdi-compass-outline" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-compass-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
@@ -57,14 +57,14 @@
:title="$t('map.tool_bearing_from_here')"
@click="$emit('bearing-from-here')"
>
- <v-icon icon="mdi-navigation-variant" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-navigation-variant" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 transition-all hover:scale-110 active:scale-90"
:title="$t('map.tool_clear')"
@click="$emit('clear')"
>
- <v-icon icon="mdi-trash-can-outline" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-trash-can-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
v-if="selectedFeature"
@@ -72,7 +72,7 @@
title="Edit note"
@click="$emit('edit-note', selectedFeature)"
>
- <v-icon icon="mdi-note-edit-outline" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-note-edit-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
v-if="selectedFeature && !selectedFeature.get('telemetry')"
@@ -80,7 +80,7 @@
title="Delete selected item"
@click="$emit('delete-feature')"
>
- <v-icon icon="mdi-selection-remove" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-selection-remove" size="18" class="sm:size-5!"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-0.5 sm:mx-1"></div>
<button
@@ -88,14 +88,14 @@
:title="$t('map.save_drawing')"
@click="$emit('save')"
>
- <v-icon icon="mdi-content-save-outline" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-content-save-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.load_drawing')"
@click="$emit('load')"
>
- <v-icon icon="mdi-folder-open-outline" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-folder-open-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-0.5 sm:mx-1"></div>
<button
@@ -103,21 +103,21 @@
:title="$t('map.go_to_my_location')"
@click="$emit('locate')"
>
- <v-icon icon="mdi-crosshairs-gps" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-crosshairs-gps" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-emerald-50 dark:hover:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.share_view')"
@click="$emit('share-view')"
>
- <v-icon icon="mdi-share-variant" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-share-variant" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-amber-50 dark:hover:bg-amber-900/20 text-amber-600 dark:text-amber-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.ping_here_toolbar')"
@click="$emit('ping-here')"
>
- <v-icon icon="mdi-send" size="18" class="sm:!size-5"></v-icon>
+ <v-icon icon="mdi-send" size="18" class="sm:size-5!"></v-icon>
</button>
</div>
</div>
diff --git a/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue b/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue
index eb226eb5..58c387c4 100644
--- a/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapExportInstructions.vue
@@ -14,7 +14,7 @@
v-for="p in presets"
:key="p.id"
type="button"
- class="px-2 py-1 text-[10px] font-bold uppercase tracking-tight rounded-lg bg-white/95 dark:bg-zinc-900/95 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 shadow-sm hover:bg-gray-50 dark:hover:bg-zinc-800"
+ class="px-2 py-1 text-[10px] font-bold uppercase tracking-tight rounded-lg bg-white/95 dark:bg-zinc-900/95 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 shadow-xs hover:bg-gray-50 dark:hover:bg-zinc-800"
@click="$emit('select-preset', p)"
>
{{ $t(`map.export_region_${p.id}`) }}
diff --git a/meshchatx/src/frontend/components/map/internal/MapLoadingOverlay.vue b/meshchatx/src/frontend/components/map/internal/MapLoadingOverlay.vue
index 12d1e3ea..249822fa 100644
--- a/meshchatx/src/frontend/components/map/internal/MapLoadingOverlay.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapLoadingOverlay.vue
@@ -1,7 +1,7 @@
<!-- SPDX-License-Identifier: 0BSD -->
<template>
- <div class="absolute inset-0 z-20 flex items-center justify-center bg-white/50 dark:bg-black/50 backdrop-blur-sm">
+ <div class="absolute inset-0 z-20 flex items-center justify-center bg-white/50 dark:bg-black/50 backdrop-blur-xs">
<div class="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-xl flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
<p class="text-gray-900 dark:text-zinc-100 font-medium">{{ message || $t("map.uploading") }}</p>
diff --git a/meshchatx/src/frontend/components/map/internal/MapSearchBar.vue b/meshchatx/src/frontend/components/map/internal/MapSearchBar.vue
index f42a57a1..517cdde7 100644
--- a/meshchatx/src/frontend/components/map/internal/MapSearchBar.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapSearchBar.vue
@@ -7,7 +7,7 @@
ref="inputEl"
:value="modelValue"
type="text"
- class="flex-1 px-4 py-2.5 bg-transparent text-gray-900 dark:text-zinc-100 placeholder-gray-400 focus:outline-none focus:ring-0 border-0 text-sm"
+ class="flex-1 px-4 py-2.5 bg-transparent text-gray-900 dark:text-zinc-100 placeholder-gray-400 focus:outline-hidden focus:ring-0 border-0 text-sm"
:placeholder="$t('map.search_placeholder')"
@input="onInput"
@keydown.enter="$emit('search')"
diff --git a/meshchatx/src/frontend/components/map/internal/MapVectorExchangePanel.vue b/meshchatx/src/frontend/components/map/internal/MapVectorExchangePanel.vue
index 5e34f56f..5a07ca6d 100644
--- a/meshchatx/src/frontend/components/map/internal/MapVectorExchangePanel.vue
+++ b/meshchatx/src/frontend/components/map/internal/MapVectorExchangePanel.vue
@@ -8,7 +8,7 @@
}}</span>
</div>
<label class="flex items-center gap-2 text-[10px] text-gray-600 dark:text-zinc-400 cursor-pointer select-none">
- <input v-model="mergeImport" type="checkbox" class="rounded border-gray-300 dark:border-zinc-600" />
+ <input v-model="mergeImport" type="checkbox" class="rounded-sm border-gray-300 dark:border-zinc-600" />
{{ $t("map.vector_exchange_merge") }}
</label>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
@@ -38,7 +38,7 @@
</button>
<button
type="button"
- class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
+ class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="disabled || !hasFeatures"
@click="$emit('export-geojson')"
>
@@ -46,7 +46,7 @@
</button>
<button
type="button"
- class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
+ class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="disabled || !hasFeatures"
@click="$emit('export-kml')"
>
@@ -54,7 +54,7 @@
</button>
<button
type="button"
- class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
+ class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="disabled || !hasFeatures"
@click="$emit('export-kmz')"
>
diff --git a/meshchatx/src/frontend/components/messages/AddAudioButton.vue b/meshchatx/src/frontend/components/messages/AddAudioButton.vue
index af88a098..09a2ea5f 100644
--- a/meshchatx/src/frontend/components/messages/AddAudioButton.vue
+++ b/meshchatx/src/frontend/components/messages/AddAudioButton.vue
@@ -5,7 +5,7 @@
<button
v-if="isRecordingAudioAttachment"
type="button"
- class="my-auto inline-flex items-center gap-x-1 rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-semibold text-red-700 shadow-sm hover:border-red-400 transition dark:border-red-500/40 dark:bg-red-900/30 dark:text-red-100"
+ class="my-auto inline-flex items-center gap-x-1 rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-semibold text-red-700 shadow-xs hover:border-red-400 transition dark:border-red-500/40 dark:bg-red-900/30 dark:text-red-100"
@click="stopRecordingAudioAttachment"
>
<MaterialDesignIcon icon-name="microphone" class="w-4 h-4" />
@@ -35,7 +35,7 @@
<div
v-if="isShowingMenu"
v-click-outside="hideMenu"
- class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none"
+ class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-hidden"
>
<div class="py-1">
<button
diff --git a/meshchatx/src/frontend/components/messages/AddImageButton.vue b/meshchatx/src/frontend/components/messages/AddImageButton.vue
index 93b74aa5..722e1e58 100644
--- a/meshchatx/src/frontend/components/messages/AddImageButton.vue
+++ b/meshchatx/src/frontend/components/messages/AddImageButton.vue
@@ -4,7 +4,7 @@
<div class="inline-flex">
<button
type="button"
- class="my-auto inline-flex items-center gap-x-1 rounded-full border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/80 px-3 py-1.5 text-xs font-semibold text-gray-800 dark:text-gray-100 shadow-sm hover:border-blue-400 dark:hover:border-blue-500 transition"
+ class="my-auto inline-flex items-center gap-x-1 rounded-full border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/80 px-3 py-1.5 text-xs font-semibold text-gray-800 dark:text-gray-100 shadow-xs hover:border-blue-400 dark:hover:border-blue-500 transition"
@click="showMenu"
>
<MaterialDesignIcon icon-name="image-plus" class="w-4 h-4" />
@@ -23,7 +23,7 @@
<div
v-if="isShowingMenu"
v-click-outside="hideMenu"
- class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none"
+ class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-hidden"
>
<div class="py-1">
<button
diff --git a/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue b/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
index 6b8af1a9..1abcfbcc 100644
--- a/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
@@ -33,7 +33,7 @@
:id="`message-${imgItem.lxmf_message.hash}`"
:key="imgItem.lxmf_message.hash"
type="button"
- class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
+ class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(cv.lxmfImageUrl(imgItem.lxmf_message.hash), cv.imageGroupGalleryUrls(entry.items))
"
@@ -63,7 +63,7 @@
:id="`message-${imgItem.lxmf_message.hash}`"
:key="imgItem.lxmf_message.hash"
type="button"
- class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
+ class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(cv.lxmfImageUrl(imgItem.lxmf_message.hash), cv.imageGroupGalleryUrls(entry.items))
"
@@ -86,7 +86,7 @@
<button
:id="`message-${cv.imageGroupSortedChron(entry.items)[2].lxmf_message.hash}`"
type="button"
- class="relative col-span-2 aspect-[2/1] max-h-52 min-h-[80px] w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
+ class="relative col-span-2 aspect-2/1 max-h-52 min-h-[80px] w-full overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(
cv.lxmfImageUrl(cv.imageGroupSortedChron(entry.items)[2].lxmf_message.hash),
@@ -120,7 +120,7 @@
:id="`message-${cell.lxmf_message.hash}`"
:key="cell.lxmf_message.hash"
type="button"
- class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
+ class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(cv.lxmfImageUrl(cell.lxmf_message.hash), cv.imageGroupGalleryUrls(entry.items))
"
@@ -152,20 +152,24 @@
class="relative rounded-2xl overflow-hidden transition-all duration-200 hover:shadow-md min-w-0 px-3 py-2"
:class="[
['cancelled', 'failed'].includes(entry.items[0].lxmf_message.state)
- ? 'shadow-sm'
+ ? 'shadow-xs'
: entry.items[0].lxmf_message.is_spam
- ? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-sm'
+ ? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-xs'
: cv.isOutboundWaitingBubble(entry.items[0])
- ? 'shadow-sm'
+ ? 'shadow-xs'
: entry.items[0].is_outbound
? cv.outboundBubbleSurfaceClass(entry.items[0])
- : 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-sm',
+ : 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-xs',
]"
:style="cv.bubbleStyles(entry.items[0])"
@contextmenu.prevent.stop="cv.onMessageContextMenu($event, entry.items[0], true)"
>
- <div class="flex items-center justify-end gap-1.5 select-none h-3">
+ <div
+ v-if="entry.showTimestamp !== false || entry.items[0].is_outbound"
+ class="flex items-center justify-end gap-1.5 select-none h-3"
+ >
<span
+ v-if="entry.showTimestamp !== false"
class="text-[9px] opacity-80 font-medium"
:class="cv.outboundBubbleFooterTimeClass(entry.items[0])"
:title="cv.getMessageInfoLines(entry.items[0].lxmf_message, entry.items[0].is_outbound).join('\n')"
@@ -193,7 +197,7 @@
<button
v-if="['failed', 'cancelled'].includes(entry.items[0].lxmf_message.state)"
type="button"
- class="ml-0.5 p-0.5 rounded hover:bg-white/20 transition-colors"
+ class="ml-0.5 p-0.5 rounded-sm hover:bg-white/20 transition-colors"
title="Retry sending"
@click.stop="cv.retrySendingMessage(entry.items[0])"
>
@@ -272,21 +276,21 @@
<div class="flex items-center gap-2">
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-600 transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-blue-600 transition-colors"
@click.stop="cv.replyToMessage(entry.items[0])"
>
{{ $t("messages.reply") }}
</button>
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-red-600 transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-red-600 transition-colors"
@click.stop="cv.deleteChatItem(entry.items[0])"
>
Delete
</button>
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-700 transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-gray-700 transition-colors"
@click.stop="cv.showRawMessage(entry.items[0])"
>
Raw LXM
@@ -316,7 +320,7 @@
<span
v-for="(r, ridx) in entry.items[0].lxmf_message.reactions"
:key="r.reactionHash || ridx"
- class="inline-flex min-h-[1.125rem] min-w-[1.125rem] cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-sm dark:border-zinc-600/90 dark:bg-zinc-900"
+ class="inline-flex min-h-4.5 min-w-4.5 cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-xs dark:border-zinc-600/90 dark:bg-zinc-900"
:style="{
order: entry.items[0].is_outbound ? ridx + 2 : ridx + 1,
}"
@@ -328,7 +332,7 @@
class="inline-flex items-center justify-center rounded-full border border-dashed border-gray-300 bg-transparent text-xs leading-none text-gray-400 hover:border-gray-400 hover:text-gray-600 hover:bg-gray-50 dark:border-zinc-600 dark:text-zinc-500 dark:hover:border-zinc-500 dark:hover:text-zinc-300 dark:hover:bg-zinc-800 transition-colors opacity-0 group-hover:opacity-100"
:class="
(entry.items[0].lxmf_message.reactions?.length ?? 0) > 0
- ? 'min-h-[1.125rem] min-w-[1.125rem] px-1 py-0'
+ ? 'min-h-4.5 min-w-4.5 px-1 py-0'
: 'h-4 w-4 min-h-0 p-0'
"
:style="{
@@ -341,6 +345,18 @@
</button>
</div>
</div>
+ <div
+ v-else-if="entry.type === 'dateDivider'"
+ class="flex justify-center w-full max-w-full my-3 shrink-0"
+ role="separator"
+ :aria-label="cv.formatDateDividerLabel(entry.dayKey)"
+ >
+ <span
+ class="inline-flex items-center rounded-full border border-gray-200/90 bg-gray-50/95 px-3 py-1 text-[11px] font-medium tracking-wide text-gray-600 shadow-xs dark:border-zinc-700/90 dark:bg-zinc-800/90 dark:text-zinc-300"
+ >
+ {{ cv.formatDateDividerLabel(entry.dayKey) }}
+ </span>
+ </div>
<div
v-for="chatItem in [entry.chatItem]"
v-else
@@ -387,7 +403,7 @@
/>
</template>
<div
- class="pointer-events-none absolute bottom-2 left-2 rounded-lg bg-black/60 px-2.5 py-1 text-xs text-white opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 sm:opacity-100"
+ class="pointer-events-none absolute bottom-2 left-2 rounded-lg bg-black/60 px-2.5 py-1 text-xs text-white opacity-0 backdrop-blur-xs transition-opacity group-hover:opacity-100 sm:opacity-100"
>
<span>{{ (chatItem.lxmf_message.fields.image.image_type ?? "image").toUpperCase() }}</span>
<span class="mx-1">·</span>
@@ -396,11 +412,11 @@
</div>
<!-- image-only: inline timestamp overlay (no bubble) -->
<div
- v-if="cv.isImageOnlyMessage(chatItem)"
+ v-if="cv.isImageOnlyMessage(chatItem) && (entry.showTimestamp !== false || chatItem.is_outbound)"
class="flex items-center gap-1.5 select-none mt-0.5"
:class="chatItem.is_outbound ? 'justify-end' : 'justify-start'"
>
- <span class="text-[9px] opacity-50 font-medium">
+ <span v-if="entry.showTimestamp !== false" class="text-[9px] opacity-50 font-medium">
{{ cv.formatTimeAgo(chatItem.lxmf_message.created_at) }}
</span>
<template v-if="chatItem.is_outbound">
@@ -429,14 +445,14 @@
class="relative rounded-2xl overflow-hidden transition-all duration-200 hover:shadow-md min-w-0"
:class="[
['cancelled', 'failed'].includes(chatItem.lxmf_message.state)
- ? 'shadow-sm'
+ ? 'shadow-xs'
: chatItem.lxmf_message.is_spam
- ? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-sm'
+ ? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-xs'
: cv.isOutboundWaitingBubble(chatItem)
- ? 'shadow-sm'
+ ? 'shadow-xs'
: chatItem.is_outbound
? cv.outboundBubbleSurfaceClass(chatItem)
- : 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-sm',
+ : 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-xs',
]"
:style="cv.bubbleStyles(chatItem)"
@click="cv.onChatItemClick(chatItem)"
@@ -493,28 +509,107 @@
</div>
<!-- content -->
- <!-- eslint-disable vue/no-v-html -->
<div
v-if="
+ chatItem.lxmf_message.content &&
+ !cv.getParsedItems(chatItem)?.isOnlyPaperMessage &&
+ !cv.getParsedItems(chatItem)?.isOnlyMapLink &&
+ !cv.shouldHideAutoImageCaption(chatItem) &&
+ cv.isMessageBodyTooLargeForDisplay(chatItem)
+ "
+ class="rounded-lg border border-amber-200/90 dark:border-amber-800/50 bg-amber-50/90 dark:bg-amber-950/25 px-3 py-2.5 space-y-2 min-w-0"
+ >
+ <div class="flex items-start gap-2">
+ <MaterialDesignIcon
+ icon-name="text-box-outline"
+ class="size-5 shrink-0 text-amber-800 dark:text-amber-300/90 mt-0.5"
+ />
+ <p class="text-xs text-amber-950 dark:text-amber-100/90 leading-relaxed min-w-0">
+ {{
+ $t("messages.oversized_body_notice", {
+ count: cv.messageBodyCharCount(chatItem),
+ })
+ }}
+ </p>
+ </div>
+ <button
+ type="button"
+ class="inline-flex items-center gap-2 rounded-lg bg-amber-700 hover:bg-amber-800 dark:bg-amber-700 dark:hover:bg-amber-600 px-3 py-2 text-xs font-semibold text-white transition-colors"
+ @click.stop="cv.copyOversizedMessageBody(chatItem)"
+ >
+ <MaterialDesignIcon icon-name="content-copy" class="size-4 shrink-0" />
+ {{ $t("messages.oversized_body_copy") }}
+ </button>
+ </div>
+ <!-- eslint-disable vue/no-v-html -->
+ <div
+ v-else-if="
chatItem.lxmf_message.content &&
!cv.getParsedItems(chatItem)?.isOnlyPaperMessage &&
!cv.getParsedItems(chatItem)?.isOnlyMapLink &&
!cv.shouldHideAutoImageCaption(chatItem)
"
- class="leading-relaxed break-words [word-break:break-word] min-w-0 markdown-content"
- :class="{
- 'markdown-content--outbound-theme': chatItem.is_outbound && cv.isThemeOutboundBubble(chatItem),
- 'markdown-content--outbound-solid': chatItem.is_outbound && !cv.isThemeOutboundBubble(chatItem),
- 'markdown-content--inbound': !chatItem.is_outbound,
- 'markdown-content--single-emoji': cv.messageMarkdownSingleEmoji(chatItem),
- }"
- :style="{
- 'font-family': 'inherit',
- 'font-size': cv.messageMarkdownFontSizePx(chatItem) + 'px',
- }"
- @click="cv.handleMessageClick"
- v-html="cv.renderMarkdown(chatItem.lxmf_message.content)"
- ></div>
+ class="min-w-0"
+ >
+ <div
+ v-if="cv.bubbleViewModel(chatItem).kind === 'loading'"
+ class="text-sm text-indigo-600/90 dark:text-indigo-300 py-0.5"
+ >
+ {{ $t("messages.translating_message") }}
+ </div>
+ <div v-else>
+ <div
+ class="leading-relaxed wrap-break-word [word-break:break-word] min-w-0 markdown-content"
+ :class="{
+ 'markdown-content--outbound-theme':
+ chatItem.is_outbound && cv.isThemeOutboundBubble(chatItem),
+ 'markdown-content--outbound-solid':
+ chatItem.is_outbound && !cv.isThemeOutboundBubble(chatItem),
+ 'markdown-content--inbound': !chatItem.is_outbound,
+ 'markdown-content--single-emoji': cv.bubbleViewModel(chatItem).singleEmoji,
+ }"
+ :style="{
+ 'font-family': 'inherit',
+ 'font-size': cv.bubbleMessageBodyFontSizePx(cv.bubbleViewModel(chatItem)) + 'px',
+ }"
+ @click="cv.handleMessageClick"
+ v-html="cv.renderMarkdown(cv.bubbleViewModel(chatItem).textForRender)"
+ ></div>
+ <div
+ v-if="cv.bubbleViewModel(chatItem).showFooter"
+ class="mt-1.5 pt-1.5 border-t border-black/5 dark:border-white/5 text-xs text-gray-500 dark:text-zinc-500"
+ >
+ <div v-if="cv.bubbleViewModel(chatItem).showOriginalLink" class="wrap-break-word">
+ <span>{{
+ $t("messages.translated_from_to", {
+ source: String(cv.bubbleViewModel(chatItem).fromCode || "").toUpperCase(),
+ target: String(cv.bubbleViewModel(chatItem).toCode || "").toUpperCase(),
+ })
+ }}</span>
+ <button
+ type="button"
+ class="ml-1.5 text-indigo-600 dark:text-indigo-400 hover:underline"
+ @click.stop="
+ cv.setBubbleMessageShowOriginal(cv.bubbleViewModel(chatItem).messageHash, true)
+ "
+ >
+ {{ $t("messages.show_original") }}
+ </button>
+ </div>
+ <div v-else-if="cv.bubbleViewModel(chatItem).showTranslationLink">
+ <button
+ type="button"
+ class="text-indigo-600 dark:text-indigo-400 hover:underline"
+ @click.stop="
+ cv.setBubbleMessageShowOriginal(cv.bubbleViewModel(chatItem).messageHash, false)
+ "
+ >
+ {{ $t("messages.show_translation") }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
<!-- eslint-enable vue/no-v-html -->
<!-- telemetry placeholder for empty content messages -->
@@ -600,7 +695,7 @@
</div>
<button
type="button"
- class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
+ class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-xs font-bold transition-colors shadow-xs"
@click="
cv.addContact(
cv.getParsedItems(chatItem).contact.name,
@@ -628,7 +723,7 @@
</p>
<button
type="button"
- class="w-full py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
+ class="w-full py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-colors shadow-xs"
@click="cv.ingestPaperMessage(cv.getParsedItems(chatItem).paperMessage)"
>
Ingest Message
@@ -654,7 +749,7 @@
</div>
<button
type="button"
- class="w-full py-2 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
+ class="w-full py-2 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-colors shadow-xs"
@click="cv.openMapShareFromParsed(cv.getParsedItems(chatItem).mapLink.parsed)"
>
{{ $t("messages.map_link_open") }}
@@ -851,8 +946,12 @@
</div>
<!-- message footer: timestamp and status icons -->
- <div class="flex items-center justify-end gap-1.5 mt-1.5 select-none h-3">
+ <div
+ v-if="entry.showTimestamp !== false || chatItem.is_outbound"
+ class="flex items-center justify-end gap-1.5 mt-1.5 select-none h-3"
+ >
<span
+ v-if="entry.showTimestamp !== false"
class="text-[9px] opacity-80 font-medium"
:class="cv.outboundBubbleFooterTimeClass(chatItem)"
:title="cv.getMessageInfoLines(chatItem.lxmf_message, chatItem.is_outbound).join('\n')"
@@ -882,7 +981,7 @@
<button
v-if="['failed', 'cancelled'].includes(chatItem.lxmf_message.state)"
type="button"
- class="ml-0.5 p-0.5 rounded hover:bg-white/20 transition-colors"
+ class="ml-0.5 p-0.5 rounded-sm hover:bg-white/20 transition-colors"
title="Retry sending"
@click.stop="cv.retrySendingMessage(chatItem)"
>
@@ -965,21 +1064,21 @@
<div class="flex items-center gap-2">
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-600 transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-blue-600 transition-colors"
@click.stop="cv.replyToMessage(chatItem)"
>
{{ $t("messages.reply") }}
</button>
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-red-600 transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-red-600 transition-colors"
@click.stop="cv.deleteChatItem(chatItem)"
>
Delete
</button>
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-700 transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-gray-700 transition-colors"
@click.stop="cv.showRawMessage(chatItem)"
>
Raw LXM
@@ -999,7 +1098,7 @@
<span
v-for="(r, ridx) in chatItem.lxmf_message.reactions"
:key="r.reactionHash || ridx"
- class="inline-flex min-h-[1.125rem] min-w-[1.125rem] cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-sm dark:border-zinc-600/90 dark:bg-zinc-900"
+ class="inline-flex min-h-4.5 min-w-4.5 cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-xs dark:border-zinc-600/90 dark:bg-zinc-900"
:style="{
order: chatItem.is_outbound ? ridx + 2 : ridx + 1,
}"
@@ -1012,7 +1111,7 @@
class="inline-flex items-center justify-center rounded-full border border-dashed border-gray-300 bg-transparent text-xs leading-none text-gray-400 hover:border-gray-400 hover:text-gray-600 hover:bg-gray-50 dark:border-zinc-600 dark:text-zinc-500 dark:hover:border-zinc-500 dark:hover:text-zinc-300 dark:hover:bg-zinc-800 transition-colors opacity-0 group-hover:opacity-100"
:class="
(chatItem.lxmf_message.reactions?.length ?? 0) > 0
- ? 'min-h-[1.125rem] min-w-[1.125rem] px-1 py-0'
+ ? 'min-h-4.5 min-w-4.5 px-1 py-0'
: 'h-4 w-4 min-h-0 p-0'
"
:style="{
diff --git a/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue b/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue
index f9dc0ac0..a5df386f 100644
--- a/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationPeerHeader.vue
@@ -4,7 +4,7 @@
<div
class="relative z-20 flex flex-wrap items-center gap-y-2 px-3 sm:px-4 py-3 border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
>
- <div class="flex-shrink-0 mr-3">
+ <div class="shrink-0 mr-3">
<LxmfUserIcon
:custom-image="selectedPeer.contact_image"
:icon-name="selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.icon_name : ''"
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index b51151c9..58dc106f 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -13,7 +13,7 @@
:style="{ background: (GlobalState?.config?.banished_color || '#dc2626') + '33' }"
>
<span
- class="banished-text !opacity-100 !text-white !shadow-lg !bg-red-600 !px-4 !py-2 !rounded-xl !border-2 !tracking-widest"
+ class="banished-text opacity-100! text-white! shadow-lg! bg-red-600! px-4! py-2! rounded-xl! border-2! tracking-widest!"
:style="{
'background-color': GlobalState?.config?.banished_color || '#dc2626',
'border-color': GlobalState?.config?.banished_color || '#dc2626',
@@ -57,7 +57,7 @@
<!-- Share Contact Modal -->
<div
v-if="isShareContactModalOpen"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="isShareContactModalOpen = false"
>
<div class="w-full max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden">
@@ -80,7 +80,7 @@
v-model="contactsSearch"
type="text"
:placeholder="$t('messages.share_contact_search_placeholder')"
- class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MaterialDesignIcon icon-name="magnify" class="size-5 text-gray-400" />
@@ -133,7 +133,7 @@
>
<div
v-show="composerImageDropActive"
- class="pointer-events-none absolute inset-0 z-[5] border-2 border-dashed border-blue-400/70 bg-blue-500/[0.06] dark:bg-blue-400/[0.08]"
+ class="pointer-events-none absolute inset-0 z-5 border-2 border-dashed border-blue-400/70 bg-blue-500/6 dark:bg-blue-400/8"
aria-hidden="true"
/>
@@ -148,7 +148,7 @@
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
- class="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0"
+ class="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0"
>
<path
stroke-linecap="round"
@@ -191,7 +191,10 @@
>
<template v-if="!useVirtualMessageList">
<div class="flex flex-col flex-col-reverse min-w-0">
- <template v-for="entry in selectedPeerChatDisplayGroups" :key="entry.key">
+ <template
+ v-for="entry in selectedPeerChatDisplayGroupsNewestFirstAugmented"
+ :key="entry.key"
+ >
<ConversationMessageEntry :entry="entry" :cv="conversationViewerSelf" />
</template>
<!-- load previous -->
@@ -199,7 +202,7 @@
v-show="!isLoadingPrevious && hasMorePrevious"
id="load-previous"
type="button"
- class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
+ class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-xs text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
@click="loadPrevious"
>
<svg
@@ -225,7 +228,7 @@
v-show="!isLoadingPrevious && hasMorePrevious"
id="load-previous"
type="button"
- class="absolute top-2 left-1/2 z-20 -translate-x-1/2 flex items-center gap-2 bg-white/95 dark:bg-zinc-950/95 backdrop-blur border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
+ class="absolute top-2 left-1/2 z-20 -translate-x-1/2 flex items-center gap-2 bg-white/95 dark:bg-zinc-950/95 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-xs text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
@click="loadPrevious"
>
<svg
@@ -246,7 +249,7 @@
</button>
<ConversationMessageListVirtual
ref="messageListVirtual"
- :groups="selectedPeerChatDisplayGroupsOldestFirst"
+ :groups="selectedPeerChatDisplayGroupsOldestFirstAugmented"
:get-scroll-element="getMessagesScrollElement"
:cv="conversationViewerSelf"
/>
@@ -261,7 +264,7 @@
>
<button
type="button"
- class="flex items-center justify-center size-8 rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur border border-gray-200 dark:border-zinc-700 shadow text-gray-500 dark:text-zinc-400 hover:bg-gray-100 dark:hover:bg-zinc-700 hover:text-gray-700 dark:hover:text-zinc-200 transition-colors"
+ class="flex items-center justify-center size-8 rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm border border-gray-200 dark:border-zinc-700 shadow-sm text-gray-500 dark:text-zinc-400 hover:bg-gray-100 dark:hover:bg-zinc-700 hover:text-gray-700 dark:hover:text-zinc-200 transition-colors"
title="Scroll to bottom"
@click="scrollMessagesToBottom"
>
@@ -287,7 +290,7 @@
}}</span>
<button
type="button"
- class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-400 dark:text-zinc-500"
+ class="p-0.5 rounded-sm hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-400 dark:text-zinc-500"
@click="closeReactionPicker"
>
<MaterialDesignIcon icon-name="close" class="size-4" />
@@ -319,7 +322,7 @@
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
- class="w-5 h-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0"
+ class="w-5 h-5 text-yellow-600 dark:text-yellow-400 shrink-0"
>
<path
stroke-linecap="round"
@@ -338,12 +341,12 @@
<!-- image attachments (mosaic, separate from text field) -->
<div
v-if="newMessageImages.length > 0"
- class="w-full max-w-[min(280px,100%)] rounded-xl overflow-hidden ring-1 ring-black/10 dark:ring-white/10 shadow-sm bg-black/5 dark:bg-white/5"
+ class="w-full max-w-[min(280px,100%)] rounded-xl overflow-hidden ring-1 ring-black/10 dark:ring-white/10 shadow-xs bg-black/5 dark:bg-white/5"
>
<div v-if="newMessageImages.length === 1" class="relative group">
<button
type="button"
- class="block w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
+ class="block w-full overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/60"
@click.stop="openImage(newMessageImageUrls[0], newMessageImageUrls)"
>
<img
@@ -364,7 +367,7 @@
<div v-for="(image, index) in newMessageImages" :key="index" class="relative group">
<button
type="button"
- class="relative block aspect-square min-h-[88px] w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
+ class="relative block aspect-square min-h-[88px] w-full overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/60"
@click.stop="openImage(newMessageImageUrls[index], newMessageImageUrls)"
>
<img
@@ -386,7 +389,7 @@
<div v-for="index in [0, 1]" :key="index" class="relative group">
<button
type="button"
- class="relative block aspect-square min-h-[88px] w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
+ class="relative block aspect-square min-h-[88px] w-full overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/60"
@click.stop="openImage(newMessageImageUrls[index], newMessageImageUrls)"
>
<img
@@ -406,7 +409,7 @@
<div class="relative group col-span-2">
<button
type="button"
- class="relative block aspect-[2/1] max-h-44 w-full min-h-[72px] overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
+ class="relative block aspect-2/1 max-h-44 w-full min-h-[72px] overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/60"
@click.stop="openImage(newMessageImageUrls[2], newMessageImageUrls)"
>
<img
@@ -432,7 +435,7 @@
>
<button
type="button"
- class="relative block aspect-square min-h-[88px] w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
+ class="relative block aspect-square min-h-[88px] w-full overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500/60"
@click.stop="openImage(newMessageImageUrls[slot - 1], newMessageImageUrls)"
>
<img
@@ -513,7 +516,7 @@
ref="message-input"
v-model="newMessageText"
:readonly="isTranslatingMessage"
- class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 block w-full min-w-0 pl-3 sm:pl-4 pr-[76px] py-2.5 resize-none shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500 min-h-[44px] max-h-[200px] overflow-y-auto leading-snug"
+ class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 block w-full min-w-0 pl-3 sm:pl-4 pr-[76px] py-2.5 resize-none shadow-xs transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500 min-h-[44px] max-h-[200px] overflow-y-auto leading-snug"
rows="1"
spellcheck="true"
:placeholder="$t('messages.send_placeholder')"
@@ -812,6 +815,62 @@
</button>
</div>
+ <!-- inline translate: target language (compose) -->
+ <div
+ v-show="translateTargetBarOpen && translateTargetModalContext?.type === 'compose'"
+ class="mt-2 flex flex-wrap items-stretch sm:items-center gap-2 rounded-xl border border-indigo-200/60 dark:border-indigo-500/30 bg-indigo-50/80 dark:bg-indigo-950/25 px-2.5 py-2"
+ >
+ <MaterialDesignIcon
+ icon-name="translate"
+ class="size-4 text-indigo-600 dark:text-indigo-400 shrink-0 self-center"
+ />
+ <label
+ class="text-xs font-semibold text-indigo-900/90 dark:text-indigo-200/90 shrink-0 self-center"
+ >{{ $t("messages.translate_select_target") }}</label
+ >
+ <select
+ v-model="translateTargetModalValue"
+ class="flex-1 min-w-0 min-h-[2.25rem] sm:min-h-0 text-sm rounded-lg border border-gray-200/90 dark:border-zinc-600 bg-white dark:bg-zinc-900 px-2.5 py-1.5 text-gray-900 dark:text-zinc-100"
+ :aria-label="$t('messages.translate_select_target')"
+ >
+ <option
+ v-for="opt in translateTargetSelectOptions"
+ :key="`c-${opt.value}`"
+ :value="opt.value"
+ >
+ {{ opt.label }}
+ </option>
+ </select>
+ <button
+ type="button"
+ class="primary-chip text-xs px-3.5 py-1.5 shrink-0"
+ :disabled="!translateTargetSelectOptions.length || isTranslateTargetModalWorking"
+ @click="confirmTranslateTargetModal"
+ >
+ <span
+ v-if="isTranslateTargetModalWorking"
+ class="inline-block w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin align-[-0.1em] mr-1.5"
+ ></span>
+ {{ $t("translator.translate") }}
+ </button>
+ <button
+ type="button"
+ class="p-1.5 shrink-0 rounded-lg text-zinc-500 hover:bg-black/5 dark:hover:bg-white/10 self-center"
+ :title="$t('common.close')"
+ :disabled="isTranslateTargetModalWorking"
+ @click="closeTranslateTargetModal"
+ >
+ <MaterialDesignIcon icon-name="close" class="size-4" />
+ </button>
+ <p
+ v-if="!translateTargetSelectOptions.length"
+ class="w-full text-xs text-amber-700/90 dark:text-amber-300/90 -mt-0.5"
+ >
+ No translation languages available yet. Open the Translator tool or enable a backend,
+ then return here.
+ </p>
+ </div>
+
<!-- action button -->
<div class="flex flex-wrap gap-2 items-center mt-2">
<button type="button" class="attachment-action-button" @click="addFilesToMessage">
@@ -868,8 +927,12 @@
v-if="hasTranslator && newMessageText"
type="button"
class="attachment-action-button"
+ :class="{
+ 'ring-1 ring-indigo-400/60 dark:ring-indigo-500/40':
+ translateTargetBarOpen && translateTargetModalContext?.type === 'compose',
+ }"
:title="$t('translator.translate')"
- @click="translateMessage"
+ @click="toggleComposeTranslateTargetBar"
>
<MaterialDesignIcon icon-name="translate" class="w-4 h-4" />
<span class="hidden sm:inline">{{ $t("translator.translate") }}</span>
@@ -902,7 +965,7 @@
:show="messageContextMenu.show"
:x="messageContextMenu.x"
:y="messageContextMenu.y"
- panel-class="z-[200]"
+ panel-class="z-200"
>
<ContextMenuItem @click="replyToMessage(messageContextMenu.chatItem)">
<MaterialDesignIcon icon-name="reply" class="size-4 text-indigo-500" />
@@ -915,6 +978,13 @@
<MaterialDesignIcon icon-name="content-copy" class="size-4 text-gray-500 dark:text-zinc-400" />
{{ $t("messages.copy_message") }}
</ContextMenuItem>
+ <ContextMenuItem
+ v-if="hasTranslator && canTranslateMessageBubbleFromMenu(messageContextMenu.chatItem)"
+ @click="openBubbleTranslateFromContextMenu"
+ >
+ <MaterialDesignIcon icon-name="translate" class="size-4 text-indigo-500" />
+ {{ $t("messages.translate_message") }}
+ </ContextMenuItem>
<div
v-if="messageContextMenu.chatItem && !messageContextMenu.chatItem.lxmf_message?.is_reaction"
class="px-3 py-2 border-t border-gray-100 dark:border-zinc-700"
@@ -1105,7 +1175,7 @@
class="group cursor-pointer p-4 bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 rounded-3xl hover:border-blue-500/50 hover:shadow-xl transition-all flex items-center gap-4"
@click="$emit('update:selectedPeer', chat)"
>
- <div class="flex-shrink-0">
+ <div class="shrink-0">
<LxmfUserIcon
:custom-image="chat.contact_image"
:icon-name="
@@ -1158,7 +1228,7 @@
v-model="composeAddress"
:readonly="isTranslatingMessage"
type="text"
- class="w-full bg-white dark:bg-zinc-900 border-2 border-gray-100 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-base rounded-3xl focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 pl-12 pr-4 py-4 shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-600 font-medium"
+ class="w-full bg-white dark:bg-zinc-900 border-2 border-gray-100 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-base rounded-3xl focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 pl-12 pr-4 py-4 shadow-xs transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-600 font-medium"
placeholder="Enter LXMF address to start a conversation..."
@keydown.enter.exact.prevent="onComposeEnterPressed"
@keydown.up.prevent="handleComposeInputUp"
@@ -1231,7 +1301,7 @@
v-if="imageModalUrl"
ref="imageModalOverlay"
tabindex="0"
- class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 dark:bg-black/90 backdrop-blur-sm p-4 outline-none"
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 dark:bg-black/90 backdrop-blur-xs p-4 outline-hidden"
@click="closeImageModal"
@keydown.left.prevent="imageModalNavigate(-1)"
@keydown.right.prevent="imageModalNavigate(1)"
@@ -1291,6 +1361,81 @@
"
/>
+ <Teleport to="body">
+ <Transition
+ enter-active-class="transition duration-200 ease-out"
+ enter-from-class="opacity-0 translate-y-2"
+ enter-to-class="opacity-100 translate-y-0"
+ leave-active-class="transition duration-150 ease-in"
+ leave-from-class="opacity-100 translate-y-0"
+ leave-to-class="opacity-0 translate-y-2"
+ >
+ <div
+ v-show="translateTargetBarOpen && translateTargetModalContext?.type === 'bubble'"
+ v-click-outside="{
+ handler: onTranslateTargetBarClickOutside,
+ capture: true,
+ }"
+ class="translate-bubble-bar fixed z-200 w-[min(calc(100%-1.25rem),24rem)] left-1/2 -translate-x-1/2 bottom-4 sm:bottom-5 pb-[max(0.25rem,env(safe-area-inset-bottom))] pointer-events-auto"
+ >
+ <div
+ class="flex flex-col gap-2 rounded-2xl border border-indigo-200/60 dark:border-indigo-500/30 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-md shadow-xl shadow-indigo-900/5 dark:shadow-black/30 px-3 py-2.5"
+ >
+ <div class="flex items-center gap-1.5 text-indigo-700/90 dark:text-indigo-300/90">
+ <MaterialDesignIcon icon-name="translate" class="size-4 shrink-0" />
+ <span class="text-sm font-semibold leading-none">{{
+ $t("messages.translate_select_target")
+ }}</span>
+ </div>
+ <div class="flex flex-wrap sm:flex-nowrap items-stretch gap-2">
+ <select
+ v-model="translateTargetModalValue"
+ class="flex-1 min-w-0 min-h-[2.5rem] text-sm rounded-lg border border-gray-200/90 dark:border-zinc-600 bg-white dark:bg-zinc-900/90 px-2.5 py-1.5 text-gray-900 dark:text-zinc-100"
+ :aria-label="$t('messages.translate_select_target')"
+ >
+ <option
+ v-for="opt in translateTargetSelectOptions"
+ :key="`b-${opt.value}`"
+ :value="opt.value"
+ >
+ {{ opt.label }}
+ </option>
+ </select>
+ <div class="flex items-center gap-1.5 w-full sm:w-auto justify-end sm:justify-start">
+ <button
+ type="button"
+ class="primary-chip text-xs px-3.5 py-2"
+ :disabled="!translateTargetSelectOptions.length || isTranslateTargetModalWorking"
+ @click="confirmTranslateTargetModal"
+ >
+ <span
+ v-if="isTranslateTargetModalWorking"
+ class="inline-block w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin align-[-0.1em] mr-1.5"
+ ></span>
+ {{ $t("translator.translate") }}
+ </button>
+ <button
+ type="button"
+ class="p-2 rounded-lg text-zinc-500 hover:bg-black/5 dark:hover:bg-white/10"
+ :title="$t('common.close')"
+ :disabled="isTranslateTargetModalWorking"
+ @click="closeTranslateTargetModal"
+ >
+ <MaterialDesignIcon icon-name="close" class="size-4" />
+ </button>
+ </div>
+ </div>
+ <p
+ v-if="!translateTargetSelectOptions.length"
+ class="text-xs text-amber-700/90 dark:text-amber-300/90 -mt-0.5"
+ >
+ No translation languages available yet. Check the translator tool in Tools.
+ </p>
+ </div>
+ </div>
+ </Transition>
+ </Teleport>
+
<!-- Raw Message Modal -->
<Transition
enter-active-class="transition ease-out duration-200"
@@ -1302,7 +1447,7 @@
>
<div
v-if="isRawMessageModalOpen"
- class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
+ class="fixed inset-0 z-150 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
@click.self="isRawMessageModalOpen = false"
>
<div
@@ -1320,7 +1465,7 @@
<MaterialDesignIcon icon-name="close" class="size-6" />
</button>
</div>
- <div class="p-0 overflow-y-auto bg-gray-50 dark:bg-zinc-950 flex-grow">
+ <div class="p-0 overflow-y-auto bg-gray-50 dark:bg-zinc-950 grow">
<div class="p-6 space-y-6">
<!-- header / status info -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
@@ -1363,7 +1508,7 @@
>Message Hash</label
>
<div
- class="text-sm font-mono break-all text-gray-900 dark:text-zinc-200 bg-white dark:bg-zinc-900 p-2 rounded border border-gray-100 dark:border-zinc-800"
+ class="text-sm font-mono break-all text-gray-900 dark:text-zinc-200 bg-white dark:bg-zinc-900 p-2 rounded-sm border border-gray-100 dark:border-zinc-800"
>
{{ rawMessageData.hash }}
</div>
@@ -1390,6 +1535,73 @@
</div>
</div>
+ <div v-if="rawMessageHasStoredPath" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+ <div class="space-y-1">
+ <label
+ class="text-[10px] font-bold uppercase tracking-wider text-gray-400 dark:text-zinc-500"
+ >{{ $t("messages.raw_path_interface_at_send") }}</label
+ >
+ <div class="text-sm text-gray-900 dark:text-zinc-200 wrap-break-word">
+ {{
+ rawMessageData.path_interface_at_send != null &&
+ rawMessageData.path_interface_at_send !== ""
+ ? rawMessageData.path_interface_at_send
+ : $t("messages.raw_path_value_unknown")
+ }}
+ </div>
+ </div>
+ <div class="space-y-1">
+ <label
+ class="text-[10px] font-bold uppercase tracking-wider text-gray-400 dark:text-zinc-500"
+ >{{ $t("messages.raw_path_hops_at_send") }}</label
+ >
+ <div class="text-sm text-gray-900 dark:text-zinc-200">
+ {{
+ rawMessageData.path_hops_at_send != null
+ ? rawMessageData.path_hops_at_send
+ : $t("messages.raw_path_value_unknown")
+ }}
+ </div>
+ </div>
+ </div>
+
+ <div
+ v-if="rawMessageData.path_finding_measure || rawMessageData.path_row_hash_hex"
+ class="grid grid-cols-1 sm:grid-cols-2 gap-4"
+ >
+ <div class="space-y-1">
+ <label
+ class="text-[10px] font-bold uppercase tracking-wider text-gray-400 dark:text-zinc-500"
+ >{{ $t("messages.raw_path_finding_measure") }}</label
+ >
+ <div class="text-sm font-mono text-gray-900 dark:text-zinc-200 wrap-break-word">
+ {{ rawMessageData.path_finding_measure || $t("messages.raw_path_value_unknown") }}
+ </div>
+ </div>
+ <div class="space-y-1">
+ <label
+ class="text-[10px] font-bold uppercase tracking-wider text-gray-400 dark:text-zinc-500"
+ >{{ $t("messages.raw_path_row_hash_rnpath") }}</label
+ >
+ <div class="flex flex-wrap items-center gap-2">
+ <div class="text-xs font-mono break-all text-gray-900 dark:text-zinc-200">
+ {{ rawMessageData.path_row_hash_hex || $t("messages.raw_path_value_unknown") }}
+ </div>
+ <button
+ v-if="rawMessageData.path_row_hash_hex"
+ type="button"
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0"
+ @click="copyHash(rawMessageData.path_row_hash_hex)"
+ >
+ {{ $t("messages.copy_hash") }}
+ </button>
+ </div>
+ <div class="text-[10px] text-gray-500 dark:text-zinc-500">
+ {{ $t("messages.raw_path_row_hash_rnpath_hint") }}
+ </div>
+ </div>
+ </div>
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div class="space-y-1">
<label
@@ -1435,10 +1647,31 @@
>Content / App Data</label
>
<div
- class="text-xs font-mono bg-white dark:bg-zinc-900 p-3 rounded border border-gray-100 dark:border-zinc-800 whitespace-pre-wrap break-all text-gray-800 dark:text-zinc-300"
+ v-if="!isRawMessageBodyOversized"
+ class="text-xs font-mono bg-white dark:bg-zinc-900 p-3 rounded-sm border border-gray-100 dark:border-zinc-800 whitespace-pre-wrap break-all text-gray-800 dark:text-zinc-300"
>
{{ rawMessageData.content }}
</div>
+ <div
+ v-else
+ class="rounded-lg border border-amber-200/90 dark:border-amber-800/50 bg-amber-50/90 dark:bg-amber-950/25 p-3 space-y-2"
+ >
+ <p class="text-xs text-amber-950 dark:text-amber-100/90 leading-relaxed">
+ {{
+ $t("messages.oversized_body_notice", {
+ count: rawMessageBodyCharCount,
+ })
+ }}
+ </p>
+ <button
+ type="button"
+ class="inline-flex items-center gap-2 rounded-lg bg-amber-700 hover:bg-amber-800 dark:bg-amber-700 dark:hover:bg-amber-600 px-3 py-2 text-xs font-semibold text-white transition-colors"
+ @click="copyRawMessageModalContent"
+ >
+ <MaterialDesignIcon icon-name="content-copy" class="size-4 shrink-0" />
+ {{ $t("messages.oversized_body_copy") }}
+ </button>
+ </div>
</div>
<div v-if="rawMessageData.raw_uri" class="space-y-1">
@@ -1447,7 +1680,7 @@
>Raw LXMF URI</label
>
<div
- class="text-[10px] font-mono bg-white dark:bg-zinc-900 p-2 rounded border border-gray-100 dark:border-zinc-800 break-all text-gray-600 dark:text-zinc-400"
+ class="text-[10px] font-mono bg-white dark:bg-zinc-900 p-2 rounded-sm border border-gray-100 dark:border-zinc-800 break-all text-gray-600 dark:text-zinc-400"
>
{{ rawMessageData.raw_uri }}
</div>
@@ -1466,7 +1699,7 @@
</summary>
<div class="mt-2 p-4 bg-black/5 dark:bg-black/20 rounded-lg overflow-x-auto">
<pre class="text-[10px] font-mono text-gray-600 dark:text-zinc-400">{{
- JSON.stringify(rawMessageData, null, 2)
+ rawMessageJsonPreviewPretty
}}</pre>
</div>
</details>
@@ -1488,6 +1721,9 @@
<script>
import Utils from "../../js/Utils";
+import { copyTextToClipboard, readTextFromClipboard } from "../../js/clipboardUtils.js";
+import { MESSAGE_BODY_MAX_DISPLAY_CHARS, isStringTooLargeForInlineDisplay } from "../../js/messageDisplayLimits.js";
+import { buildTimestampGroupedOldestFirst } from "../../js/messageTimestampGrouping.js";
import DownloadUtils from "../../js/DownloadUtils";
import { clampFloatingToViewport } from "../../js/clampFloatingToViewport.js";
import { isNearBottom, scrollContainerToBottom, shouldLoadPreviousMessages } from "./conversationScroll.js";
@@ -1503,6 +1739,7 @@ import ConversationMessageEntry from "./ConversationMessageEntry.vue";
import ConversationMessageListVirtual from "./ConversationMessageListVirtual.vue";
import { displayGroupsOldestFirst, MIN_VIRTUAL_DISPLAY_GROUPS } from "./messageListVirtual.js";
import DialogUtils from "../../js/DialogUtils";
+import { getDestinationPath, postRequestPath } from "../../js/reticulumPathfinding.js";
import MicrophoneRecorder from "../../js/MicrophoneRecorder";
import WebSocketConnection from "../../js/WebSocketConnection";
import AddAudioButton from "./AddAudioButton.vue";
@@ -1643,6 +1880,12 @@ export default {
rawMessageData: null,
hasTranslator: false,
translatorLanguages: [],
+ translateTargetBarOpen: false,
+ translateTargetModalValue: "en",
+ translateTargetModalContext: null,
+ isTranslateTargetModalWorking: false,
+ bubbleTranslateBarIgnoreOutsideUntil: 0,
+ messageBubbleTranslation: {},
propagationNodeStatus: null,
propagationStatusInterval: null,
@@ -1717,6 +1960,37 @@ export default {
transform: "translateX(-50%)",
};
},
+ isRawMessageBodyOversized() {
+ return isStringTooLargeForInlineDisplay(this.rawMessageData?.content);
+ },
+ rawMessageBodyCharCount() {
+ const c = this.rawMessageData?.content;
+ return typeof c === "string" ? c.length : 0;
+ },
+ rawMessageJsonPreviewPretty() {
+ if (!this.rawMessageData) {
+ return "";
+ }
+ const d = { ...this.rawMessageData };
+ const c = d.content;
+ if (typeof c === "string" && c.length > MESSAGE_BODY_MAX_DISPLAY_CHARS) {
+ d.content = `[Omitted ${c.length} characters — use Copy full text above]`;
+ }
+ try {
+ return JSON.stringify(d, null, 2);
+ } catch {
+ return "";
+ }
+ },
+ rawMessageHasStoredPath() {
+ const d = this.rawMessageData;
+ if (!d) {
+ return false;
+ }
+ const hops = d.path_hops_at_send;
+ const iface = d.path_interface_at_send;
+ return hops != null || (iface != null && iface !== "");
+ },
usesThemeOutboundBubbleColor() {
const c = GlobalState?.config?.message_outbound_bubble_color;
if (c == null || String(c).trim() === "") {
@@ -1726,6 +2000,7 @@ export default {
},
bubbleStyles() {
void GlobalState.detailedOutboundSendStatus;
+ void GlobalState.messageTimestampGroupingEnabled;
void this.sendStatusUiMs;
void this.usesThemeOutboundBubbleColor;
void GlobalState.config?.theme;
@@ -1880,12 +2155,13 @@ export default {
selectedPeerChatItems() {
// get all chat items related to the selected peer
if (this.selectedPeer) {
+ const peer = (this.selectedPeer.destination_hash || "").toLowerCase();
return this.chatItems.filter((chatItem) => {
if (chatItem.type === "lxmf_message") {
- const isFromSelectedPeer =
- chatItem.lxmf_message.source_hash === this.selectedPeer.destination_hash;
- const isToSelectedPeer =
- chatItem.lxmf_message.destination_hash === this.selectedPeer.destination_hash;
+ const src = (chatItem.lxmf_message.source_hash || "").toLowerCase();
+ const dst = (chatItem.lxmf_message.destination_hash || "").toLowerCase();
+ const isFromSelectedPeer = src === peer;
+ const isToSelectedPeer = dst === peer;
if (!(isFromSelectedPeer || isToSelectedPeer)) return false;
@@ -1914,13 +2190,14 @@ export default {
},
selectedPeerTelemetryItems() {
if (!this.selectedPeer) return [];
+ const peer = (this.selectedPeer.destination_hash || "").toLowerCase();
return this.chatItems
.filter((chatItem) => {
if (chatItem.type === "lxmf_message") {
- const isFromSelectedPeer =
- chatItem.lxmf_message.source_hash === this.selectedPeer.destination_hash;
- const isToSelectedPeer =
- chatItem.lxmf_message.destination_hash === this.selectedPeer.destination_hash;
+ const src = (chatItem.lxmf_message.source_hash || "").toLowerCase();
+ const dst = (chatItem.lxmf_message.destination_hash || "").toLowerCase();
+ const isFromSelectedPeer = src === peer;
+ const isToSelectedPeer = dst === peer;
if (!(isFromSelectedPeer || isToSelectedPeer)) return false;
@@ -1969,6 +2246,21 @@ export default {
selectedPeerChatDisplayGroupsOldestFirst() {
return displayGroupsOldestFirst(this.selectedPeerChatDisplayGroups);
},
+ selectedPeerChatDisplayGroupsOldestFirstAugmented() {
+ void GlobalState.messageTimestampGroupingEnabled;
+ const base = this.selectedPeerChatDisplayGroupsOldestFirst;
+ if (!GlobalState.messageTimestampGroupingEnabled) {
+ return buildTimestampGroupedOldestFirst(base, { groupingEnabled: false });
+ }
+ return buildTimestampGroupedOldestFirst(base);
+ },
+ selectedPeerChatDisplayGroupsNewestFirstAugmented() {
+ const a = this.selectedPeerChatDisplayGroupsOldestFirstAugmented;
+ if (!a?.length) {
+ return [];
+ }
+ return a.slice().reverse();
+ },
conversationViewerSelf() {
return this;
},
@@ -1980,11 +2272,30 @@ export default {
return GlobalState?.config?.message_list_virtualization !== false;
},
oldestMessageId() {
- if (this.selectedPeerChatItems.length > 0) {
- return this.selectedPeerChatItems[0].lxmf_message.id;
+ if (!this.selectedPeer) {
+ return null;
}
-
- return null;
+ const peer = (this.selectedPeer.destination_hash || "").toLowerCase();
+ let minId = null;
+ for (const chatItem of this.chatItems) {
+ if (chatItem.type !== "lxmf_message") {
+ continue;
+ }
+ const m = chatItem.lxmf_message;
+ const src = (m.source_hash || "").toLowerCase();
+ const dst = (m.destination_hash || "").toLowerCase();
+ if (src !== peer && dst !== peer) {
+ continue;
+ }
+ const id = m.id;
+ if (id == null) {
+ continue;
+ }
+ if (minId == null || id < minId) {
+ minId = id;
+ }
+ }
+ return minId;
},
hasFailedOrCancelledMessages() {
return this.selectedPeerChatItems.some(
@@ -1995,10 +2306,26 @@ export default {
const h = this.selectedPeer?.destination_hash || "default";
return h.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 24) || "default";
},
+ translateTargetSelectOptions() {
+ const seen = new Map();
+ for (const l of this.translatorLanguages || []) {
+ if (l && l.code) {
+ const c = String(l.code).toLowerCase().slice(0, 8);
+ if (!seen.has(c)) {
+ const name = l.name || c;
+ seen.set(c, { value: c, label: `${name} (${c})` });
+ }
+ }
+ }
+ return Array.from(seen.values()).sort((a, b) =>
+ a.label.localeCompare(b.label, undefined, { sensitivity: "base" })
+ );
+ },
},
watch: {
selectedPeer: {
handler(newPeer, oldPeer) {
+ this.messageBubbleTranslation = {};
if (oldPeer) {
this.saveDraft(oldPeer.destination_hash);
}
@@ -2165,6 +2492,9 @@ export default {
if (!c) {
return false;
}
+ if (this.isMessageBodyTooLargeForDisplay(chatItem)) {
+ return false;
+ }
if (this.getParsedItems(chatItem)?.isOnlyPaperMessage) {
return false;
}
@@ -2254,12 +2584,11 @@ export default {
GlobalEmitter.emit("sync-propagation-node");
},
async copyMyAddress() {
- try {
- await navigator.clipboard.writeText(this.myLxmfAddressHash);
+ const ok = await copyTextToClipboard(this.myLxmfAddressHash);
+ if (ok) {
ToastUtils.success(this.$t("messages.address_copied"));
- } catch (e) {
- console.error(e);
- ToastUtils.error(this.$t("messages.failed_copy_address"));
+ } else {
+ ToastUtils.error(this.$t("messages.clipboard_write_unavailable"));
}
},
focusComposeInput() {
@@ -2279,31 +2608,246 @@ export default {
}
},
async checkTranslator() {
- if (!this.config?.translator_enabled) {
+ const argosOn = this.config?.translator_argos_enabled;
+ const libreOn = this.config?.translator_libretranslate_enabled;
+ if (!argosOn && !libreOn) {
this.hasTranslator = false;
+ this.translatorLanguages = [];
return;
}
try {
const response = await window.api.get("/api/v1/translator/languages");
- this.translatorLanguages = response.data.languages || [];
+ const raw = response.data.languages || [];
+ this.translatorLanguages = raw.filter(
+ (lang) => (lang.source === "argos" && argosOn) || (lang.source === "libretranslate" && libreOn)
+ );
this.hasTranslator = this.translatorLanguages.length > 0;
} catch (e) {
console.log("Failed to check translator:", e);
this.hasTranslator = false;
}
},
- async translateMessage() {
- if (!this.newMessageText || this.isTranslatingMessage) return;
-
+ normalizedLocaleCode(loc) {
+ if (loc == null) {
+ return "en";
+ }
+ const s = String(loc);
+ const t = s.split(/[-_]/)[0] || s;
+ const c = t.toLowerCase();
+ if (c.length < 2) {
+ return "en";
+ }
+ return c.slice(0, 2);
+ },
+ composeSourceLangForTranslate() {
+ const cfg = this.config;
+ const a = cfg?.translator_argos_enabled;
+ const l = cfg?.translator_libretranslate_enabled;
+ const onlyLibre = Boolean(l) && !a;
+ if (onlyLibre) {
+ return "auto";
+ }
+ return this.normalizedLocaleCode(cfg?.language || this.$i18n.locale) || "en";
+ },
+ _readSavedTranslateTargetLang() {
+ let v = null;
+ try {
+ v =
+ localStorage.getItem("meshchatx.translateTargetLang") ||
+ localStorage.getItem("meshchatx.composeTranslateTargetLang");
+ } catch {
+ v = null;
+ }
+ if (!v) {
+ return null;
+ }
+ return String(v).toLowerCase().slice(0, 8);
+ },
+ _persistTranslateTargetLang(targetLang) {
+ const t = String(targetLang || "")
+ .toLowerCase()
+ .slice(0, 8);
+ if (!t) {
+ return;
+ }
+ try {
+ localStorage.setItem("meshchatx.translateTargetLang", t);
+ localStorage.setItem("meshchatx.composeTranslateTargetLang", t);
+ } catch {
+ /* empty */
+ }
+ },
+ defaultTranslateTargetForModal() {
+ const source = String(this.composeSourceLangForTranslate() || "").toLowerCase();
+ const opts = this.translateTargetSelectOptions;
+ if (!opts.length) {
+ return "en";
+ }
+ const fromStorage = this._readSavedTranslateTargetLang();
+ if (fromStorage && opts.some((o) => o.value === fromStorage) && fromStorage !== source) {
+ return fromStorage;
+ }
+ const diff = opts.find((o) => o.value !== source);
+ if (diff) {
+ return diff.value;
+ }
+ return opts[0].value;
+ },
+ defaultBubbleTranslateTargetForModal() {
+ const source = this._bubbleTranslateSourcePreview();
+ const opts = this.translateTargetSelectOptions;
+ if (!opts.length) {
+ return "en";
+ }
+ const fromStorage = this._readSavedTranslateTargetLang();
+ if (fromStorage && opts.some((o) => o.value === fromStorage)) {
+ if (String(source).toLowerCase() === "auto") {
+ return fromStorage;
+ }
+ const s2 = String(source).toLowerCase().slice(0, 2);
+ if (fromStorage !== s2) {
+ return fromStorage;
+ }
+ }
+ if (String(source).toLowerCase() === "auto") {
+ return opts[0].value;
+ }
+ const s2 = String(source).toLowerCase().slice(0, 2);
+ const diff = opts.find((o) => o.value !== s2);
+ return diff ? diff.value : opts[0].value;
+ },
+ _bubbleTranslateSourcePreview() {
+ const cfg = this.config;
+ const a = cfg?.translator_argos_enabled;
+ const l = cfg?.translator_libretranslate_enabled;
+ if (l && !a) {
+ return "auto";
+ }
+ return this.normalizedLocaleCode(cfg?.language || this.$i18n.locale) || "en";
+ },
+ onTranslateTargetBarClickOutside() {
+ if (this.isTranslateTargetModalWorking) {
+ return;
+ }
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
+ if (now < this.bubbleTranslateBarIgnoreOutsideUntil) {
+ return;
+ }
+ if (this.translateTargetModalContext?.type === "bubble") {
+ this.closeTranslateTargetModal();
+ }
+ },
+ async toggleComposeTranslateTargetBar() {
+ if (this.translateTargetBarOpen && this.translateTargetModalContext?.type === "compose") {
+ this.closeTranslateTargetModal();
+ return;
+ }
+ if (!this.newMessageText?.trim() || this.isTranslatingMessage) {
+ return;
+ }
+ if (!this.translatorLanguages || this.translatorLanguages.length === 0) {
+ await this.checkTranslator();
+ }
+ this.translateTargetModalContext = { type: "compose" };
+ this.translateTargetModalValue = this.defaultTranslateTargetForModal();
+ this.translateTargetBarOpen = true;
+ },
+ openBubbleTranslateFromContextMenu() {
+ const chatItem = this.messageContextMenu.chatItem;
+ this.messageContextMenu.show = false;
+ if (!chatItem || !this.canTranslateMessageBubbleFromMenu(chatItem)) {
+ return;
+ }
+ const doOpen = async () => {
+ if (!this.translatorLanguages || this.translatorLanguages.length === 0) {
+ await this.checkTranslator();
+ }
+ if (!this.canTranslateMessageBubbleFromMenu(chatItem)) {
+ return;
+ }
+ const t = typeof performance !== "undefined" ? performance.now() : Date.now();
+ this.bubbleTranslateBarIgnoreOutsideUntil = t + 500;
+ this.translateTargetModalContext = { type: "bubble", chatItem };
+ this.translateTargetModalValue = this.defaultBubbleTranslateTargetForModal();
+ this.translateTargetBarOpen = true;
+ };
+ queueMicrotask(() => {
+ void doOpen();
+ });
+ },
+ closeTranslateTargetModal() {
+ if (this.isTranslateTargetModalWorking) {
+ return;
+ }
+ this.translateTargetBarOpen = false;
+ this.translateTargetModalContext = null;
+ },
+ async confirmTranslateTargetModal() {
+ if (this.isTranslateTargetModalWorking || !this.translateTargetSelectOptions.length) {
+ return;
+ }
+ const targetLang = this.translateTargetModalValue;
+ const ctx = this.translateTargetModalContext;
+ if (!ctx) {
+ return;
+ }
+ this.isTranslateTargetModalWorking = true;
+ try {
+ this._persistTranslateTargetLang(targetLang);
+ if (ctx.type === "compose") {
+ await this.applyComposeTranslation(targetLang);
+ } else if (ctx.type === "bubble" && ctx.chatItem) {
+ await this.applyBubbleMessageTranslation(ctx.chatItem, targetLang);
+ }
+ this.translateTargetBarOpen = false;
+ this.translateTargetModalContext = null;
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.isTranslateTargetModalWorking = false;
+ }
+ },
+ canTranslateMessageBubbleFromMenu(chatItem) {
+ if (!this.hasTranslator || !chatItem || chatItem.lxmf_message?.is_reaction) {
+ return false;
+ }
+ const t = this.copyableMessagePlainText(chatItem);
+ if (!t) {
+ return false;
+ }
+ if (this.isMessageBodyTooLargeForDisplay(chatItem)) {
+ return false;
+ }
+ return true;
+ },
+ async applyComposeTranslation(targetLang) {
+ if (!this.newMessageText?.trim()) {
+ return;
+ }
+ const cfg = this.config;
+ const a = cfg?.translator_argos_enabled;
+ const l = cfg?.translator_libretranslate_enabled;
+ if (!a && !l) {
+ return;
+ }
+ const useArgos = Boolean(a) && !l;
+ const onlyLibre = Boolean(l) && !a;
+ const target = String(targetLang).toLowerCase().slice(0, 8);
+ const source_lang = onlyLibre
+ ? "auto"
+ : this.normalizedLocaleCode(cfg?.language || this.$i18n.locale) || "en";
+ if (useArgos && source_lang && target && source_lang === target) {
+ ToastUtils.error(this.$t("messages.translate_target_invalid"));
+ throw new Error("target");
+ }
+ this.isTranslatingMessage = true;
try {
- this.isTranslatingMessage = true;
- const targetLang = this.$i18n.locale || "en";
const response = await window.api.post("/api/v1/translator/translate", {
text: this.newMessageText,
- source_lang: "auto",
- target_lang: targetLang,
+ source_lang,
+ target_lang: target,
+ use_argos: useArgos,
});
-
if (response.data.translated_text) {
this.newMessageText = response.data.translated_text;
this.$nextTick(() => {
@@ -2311,12 +2855,120 @@ export default {
});
}
} catch (e) {
- console.error("Translation failed:", e);
- ToastUtils.error(this.$t("messages.translation_failed"));
+ if (e?.message !== "target") {
+ console.error("Translation failed:", e);
+ ToastUtils.error(this.$t("messages.translation_failed"));
+ }
+ throw e;
} finally {
this.isTranslatingMessage = false;
}
},
+ async applyBubbleMessageTranslation(chatItem, targetLang) {
+ const hash = chatItem?.lxmf_message?.hash;
+ const text = this.copyableMessagePlainText(chatItem);
+ if (!hash || !text) {
+ return;
+ }
+ const cfg = this.config;
+ const a = cfg?.translator_argos_enabled;
+ const l = cfg?.translator_libretranslate_enabled;
+ if (!a && !l) {
+ return;
+ }
+ const useArgos = Boolean(a) && !l;
+ const onlyLibre = Boolean(l) && !a;
+ const target = String(targetLang).toLowerCase().slice(0, 8);
+ const source_lang = onlyLibre
+ ? "auto"
+ : this.normalizedLocaleCode(cfg?.language || this.$i18n.locale) || "en";
+ if (useArgos && source_lang && target && source_lang === target) {
+ ToastUtils.error(this.$t("messages.translate_target_invalid"));
+ return;
+ }
+ this.messageBubbleTranslation[hash] = {
+ loading: true,
+ showOriginal: false,
+ translatedText: "",
+ resolvedSourceLang: "",
+ targetLang: target,
+ source: "",
+ };
+ try {
+ const response = await window.api.post("/api/v1/translator/translate", {
+ text,
+ source_lang,
+ target_lang: target,
+ use_argos: useArgos,
+ });
+ const data = response.data;
+ this.messageBubbleTranslation[hash] = {
+ loading: false,
+ showOriginal: false,
+ translatedText: data.translated_text || "",
+ resolvedSourceLang: data.source_lang || source_lang,
+ targetLang: data.target_lang || target,
+ source: data.source || "",
+ };
+ } catch (e) {
+ delete this.messageBubbleTranslation[hash];
+ console.error("Translation failed:", e);
+ ToastUtils.error(this.$t("messages.translation_failed"));
+ }
+ },
+ setBubbleMessageShowOriginal(hash, show) {
+ const t = this.messageBubbleTranslation[hash];
+ if (!t || !t.translatedText) {
+ return;
+ }
+ this.messageBubbleTranslation[hash] = {
+ ...t,
+ showOriginal: show,
+ };
+ },
+ bubbleViewModel(chatItem) {
+ const c = chatItem.lxmf_message?.content;
+ if (!c) {
+ return { kind: "html", textForRender: "", singleEmoji: false, showFooter: false };
+ }
+ const h = chatItem.lxmf_message.hash;
+ const tr = this.messageBubbleTranslation[h];
+ if (tr?.loading) {
+ return { kind: "loading" };
+ }
+ const hasTr = tr && tr.translatedText;
+ const showingTranslated = hasTr && !tr.showOriginal;
+ const textForRender = showingTranslated ? tr.translatedText : c;
+ let singleEmoji = this.messageMarkdownSingleEmoji(chatItem);
+ if (showingTranslated) {
+ singleEmoji =
+ !this.isMessageBodyTooLargeForDisplay(chatItem) &&
+ MarkdownRenderer.isSingleEmojiMessage(tr.translatedText);
+ }
+ return {
+ kind: "html",
+ textForRender,
+ isTranslatedView: showingTranslated,
+ showFooter: Boolean(hasTr),
+ showOriginalLink: hasTr && !tr.showOriginal,
+ showTranslationLink: hasTr && tr.showOriginal,
+ fromCode: (tr && tr.resolvedSourceLang) || "–",
+ toCode: (tr && tr.targetLang) || "–",
+ messageHash: h,
+ singleEmoji,
+ };
+ },
+ bubbleMessageBodyFontSizePx(vm) {
+ const base = Number(this.config?.message_font_size) || 14;
+ if (vm && vm.singleEmoji) {
+ return Math.round(base * 2.75);
+ }
+ return base;
+ },
+ async translateMessage() {
+ this.translateTargetModalValue = this.defaultTranslateTargetForModal();
+ await this.applyComposeTranslation(this.translateTargetModalValue);
+ },
adjustTextareaHeight() {
const textarea = this.$refs["message-input"];
if (textarea) {
@@ -2484,11 +3136,14 @@ export default {
// convert lxmf messages to chat items
const chatItems = [];
- const lxmfMessages = mergeLxmfReactionRowsIntoMessages(response.data.lxmf_messages);
+ const rawList = response.data?.lxmf_messages;
+ const lxmfMessages = mergeLxmfReactionRowsIntoMessages(Array.isArray(rawList) ? rawList : []);
+ const myHash = (this.myLxmfAddressHash || "").toLowerCase();
for (const lxmfMessage of lxmfMessages) {
+ const src = (lxmfMessage.source_hash || "").toLowerCase();
chatItems.push({
type: "lxmf_message",
- is_outbound: this.myLxmfAddressHash === lxmfMessage.source_hash,
+ is_outbound: myHash !== "" && myHash === src,
lxmf_message: lxmfMessage,
});
}
@@ -2503,7 +3158,17 @@ export default {
const prevScrollHeight = scrollEl ? scrollEl.scrollHeight : 0;
const prevScrollTop = scrollEl ? scrollEl.scrollTop : 0;
+ const seenHashes = new Set(
+ this.chatItems.map((c) => c.lxmf_message?.hash).filter((h) => h != null && h !== "")
+ );
for (const chatItem of chatItems) {
+ const h = chatItem.lxmf_message?.hash;
+ if (h && seenHashes.has(h)) {
+ continue;
+ }
+ if (h) {
+ seenHashes.add(h);
+ }
this.chatItems.unshift(chatItem);
}
@@ -2775,8 +3440,11 @@ export default {
}
return normalized;
},
+ _hexEqual(a, b) {
+ return (a || "").toLowerCase() === (b || "").toLowerCase();
+ },
onLxmfMessageReceived(lxmfMessage) {
- if (lxmfMessage.source_hash !== this.selectedPeer?.destination_hash) {
+ if (!this._hexEqual(lxmfMessage.source_hash, this.selectedPeer?.destination_hash)) {
return;
}
@@ -2803,7 +3471,7 @@ export default {
this.autoLoadAudioAttachments();
},
onLxmfMessageCreated(lxmfMessage) {
- if (lxmfMessage.destination_hash !== this.selectedPeer?.destination_hash) {
+ if (!this._hexEqual(lxmfMessage.destination_hash, this.selectedPeer?.destination_hash)) {
return;
}
@@ -2821,8 +3489,8 @@ export default {
},
onLxmfMessageUpdated(lxmfMessage) {
const lxmfMessageHash = lxmfMessage.hash;
- const chatItemIndex = this.chatItems.findIndex(
- (chatItem) => chatItem.lxmf_message?.hash === lxmfMessageHash
+ const chatItemIndex = this.chatItems.findIndex((chatItem) =>
+ this._hexEqual(chatItem.lxmf_message?.hash, lxmfMessageHash)
);
if (chatItemIndex === -1) {
return;
@@ -2835,26 +3503,18 @@ export default {
},
onLxmfMessageDeleted(hash) {
if (hash) {
- // remove existing chat item by lxmf message hash
this.chatItems = this.chatItems.filter((item) => {
- return item.lxmf_message?.hash !== hash;
+ return !this._hexEqual(item.lxmf_message?.hash, hash);
});
}
},
async getPeerPath() {
if (this.selectedPeer) {
try {
- // get path to destination
- const response = await window.api.get(
- `/api/v1/destination/${this.selectedPeer.destination_hash}/path`
- );
-
- // update ui
+ const response = await getDestinationPath(window.api, this.selectedPeer.destination_hash, {});
this.selectedPeerPath = response.data.path;
} catch (e) {
console.log(e);
-
- // clear previous known path
this.selectedPeerPath = null;
}
}
@@ -2961,18 +3621,25 @@ export default {
}
scrollContainerToBottom(container);
};
- pump();
+ const safePump = () => {
+ try {
+ pump();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+ safePump();
if (!this.useVirtualMessageList) {
requestAnimationFrame(() => {
if (stale()) return;
- pump();
+ safePump();
this.messagesViewportReady = true;
});
} else {
let passes = 0;
const settle = () => {
if (stale()) return;
- pump();
+ safePump();
passes++;
if (isNearBottom(container) || passes >= 6) {
this.messagesViewportReady = true;
@@ -2987,7 +3654,7 @@ export default {
},
isLxmfMessageInUi: function (hash) {
- return this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === hash) !== -1;
+ return this.chatItems.findIndex((chatItem) => this._hexEqual(chatItem.lxmf_message?.hash, hash)) !== -1;
},
async getCustomDisplayName() {
if (this.selectedPeer) {
@@ -3106,12 +3773,43 @@ export default {
if (!text) {
return;
}
- try {
- await navigator.clipboard.writeText(text);
+ const ok = await copyTextToClipboard(text);
+ if (ok) {
ToastUtils.success(this.$t("messages.message_copied"));
this.messageContextMenu.show = false;
- } catch {
- ToastUtils.error(this.$t("messages.failed_copy_message"));
+ } else {
+ ToastUtils.error(this.$t("messages.clipboard_write_unavailable"));
+ }
+ },
+ isMessageBodyTooLargeForDisplay(chatItem) {
+ return isStringTooLargeForInlineDisplay(chatItem?.lxmf_message?.content);
+ },
+ messageBodyCharCount(chatItem) {
+ const c = chatItem?.lxmf_message?.content;
+ return typeof c === "string" ? c.length : 0;
+ },
+ async copyOversizedMessageBody(chatItem) {
+ const text = this.copyableMessagePlainText(chatItem);
+ if (!text) {
+ return;
+ }
+ const ok = await copyTextToClipboard(text);
+ if (ok) {
+ ToastUtils.success(this.$t("messages.oversized_body_copied"));
+ } else {
+ ToastUtils.error(this.$t("messages.clipboard_write_unavailable"));
+ }
+ },
+ async copyRawMessageModalContent() {
+ const c = this.rawMessageData?.content;
+ if (typeof c !== "string" || c.length === 0) {
+ return;
+ }
+ const ok = await copyTextToClipboard(c);
+ if (ok) {
+ ToastUtils.success(this.$t("messages.oversized_body_copied"));
+ } else {
+ ToastUtils.error(this.$t("messages.clipboard_write_unavailable"));
}
},
replyToMessage(chatItem) {
@@ -3320,17 +4018,16 @@ export default {
});
},
async showRawMessage(chatItem) {
- try {
- // we'll try to get the URI first as it contains the raw signed message
- const response = await window.api.get(`/api/v1/lxmf-messages/${chatItem.lxmf_message.hash}/uri`);
- this.rawMessageData = {
- ...chatItem.lxmf_message,
- raw_uri: response.data.uri,
- };
- } catch {
- // if URI is not available (message no longer in router), we show what we have
- this.rawMessageData = { ...chatItem.lxmf_message };
- }
+ const base = { ...chatItem.lxmf_message };
+ const uriPromise = window.api
+ .get(`/api/v1/lxmf-messages/${chatItem.lxmf_message.hash}/uri`)
+ .then((r) => r.data?.uri ?? null)
+ .catch(() => null);
+ const rawUri = await uriPromise;
+ this.rawMessageData = {
+ ...base,
+ raw_uri: rawUri ?? undefined,
+ };
this.isRawMessageModalOpen = true;
},
async downloadAndDecodeAudio(chatItem) {
@@ -3647,9 +4344,9 @@ export default {
return "";
}
if (!this.usesThemeOutboundBubbleColor) {
- return "shadow-sm";
+ return "shadow-xs";
}
- return "shadow-sm bg-sky-100 text-slate-900 border border-sky-200/90 dark:bg-sky-950/45 dark:text-sky-50 dark:border-sky-800/55";
+ return "shadow-xs bg-sky-100 text-slate-900 border border-sky-200/90 dark:bg-sky-950/45 dark:text-sky-50 dark:border-sky-800/55";
},
outboundBubbleFooterTimeClass(chatItem) {
if (!chatItem.is_outbound) {
@@ -3801,7 +4498,7 @@ export default {
return;
}
try {
- await window.api.post(`/api/v1/destination/${this.selectedPeer.destination_hash}/request-path`);
+ await postRequestPath(window.api, this.selectedPeer.destination_hash);
} catch (e) {
console.log(e);
}
@@ -4408,12 +5105,11 @@ export default {
if (!uri) {
return;
}
- try {
- await navigator.clipboard.writeText(uri);
+ const ok = await copyTextToClipboard(uri);
+ if (ok) {
ToastUtils.success(this.$t("messages.map_link_copied"));
- } catch (e) {
- console.error(e);
- ToastUtils.error(this.$t("messages.failed_copy_address"));
+ } else {
+ ToastUtils.error(this.$t("messages.clipboard_write_unavailable"));
}
},
isTelemetryOnly(msg) {
@@ -4447,16 +5143,53 @@ export default {
// Using this.now ensures the computed value updates when the timer ticks
return this.now ? Utils.formatTimeAgo(datetimeString) : Utils.formatTimeAgo(datetimeString);
},
+ formatDateDividerLabel(dayKey) {
+ if (!dayKey || typeof dayKey !== "string") {
+ return "";
+ }
+ const p = dayKey.split("-").map((x) => parseInt(x, 10));
+ if (p.length !== 3 || p.some((n) => Number.isNaN(n))) {
+ return dayKey;
+ }
+ const d = new Date(p[0], p[1] - 1, p[2]);
+ if (Number.isNaN(d.getTime())) {
+ return dayKey;
+ }
+ const startOf = (dt) => {
+ const x = new Date(dt);
+ x.setHours(0, 0, 0, 0);
+ return x.getTime();
+ };
+ const today = new Date();
+ if (startOf(d) === startOf(today)) {
+ return this.$t("messages.date_divider_today");
+ }
+ const y = new Date(today);
+ y.setDate(y.getDate() - 1);
+ if (startOf(d) === startOf(y)) {
+ return this.$t("messages.date_divider_yesterday");
+ }
+ try {
+ const loc = typeof this.$i18n?.locale === "string" ? this.$i18n.locale : "en";
+ return new Intl.DateTimeFormat(loc, {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ }).format(d);
+ } catch {
+ return dayKey;
+ }
+ },
formatDestinationHash(hash) {
return Utils.formatDestinationHash(hash);
},
async copyHash(hash) {
- try {
- await navigator.clipboard.writeText(hash);
+ const ok = await copyTextToClipboard(hash);
+ if (ok) {
ToastUtils.success(this.$t("messages.hash_copied"));
- } catch (e) {
- console.error(e);
- ToastUtils.error(this.$t("messages.failed_to_copy_hash"));
+ } else {
+ ToastUtils.error(this.$t("messages.clipboard_write_unavailable"));
}
},
formatBytes: function (bytes) {
@@ -4539,28 +5272,35 @@ export default {
this.attachPastedOrDroppedImageFiles(imageFiles, "drop");
},
async pasteFromClipboard() {
- try {
- const text = await navigator.clipboard.readText();
- if (text) {
- const input = this.$refs["message-input"];
- const start = input.selectionStart;
- const end = input.selectionEnd;
- const currentText = this.newMessageText || "";
- this.newMessageText = currentText.substring(0, start) + text + currentText.substring(end);
-
- this.$nextTick(() => {
- input.focus();
- const newCursorPos = start + text.length;
- input.setSelectionRange(newCursorPos, newCursorPos);
- // adjust height
- input.style.height = "auto";
- input.style.height = Math.min(input.scrollHeight, 200) + "px";
- });
+ const result = await readTextFromClipboard();
+ if (!result.ok) {
+ if (result.code === "insecure_context") {
+ ToastUtils.error(this.$t("messages.clipboard_read_requires_secure_context"));
+ } else {
+ ToastUtils.error(this.$t("messages.failed_read_clipboard"));
}
- } catch (err) {
- console.error("Failed to read clipboard contents: ", err);
- ToastUtils.error(this.$t("messages.failed_read_clipboard"));
+ return;
+ }
+ const text = result.text;
+ if (!text) {
+ return;
+ }
+ const input = this.$refs["message-input"];
+ if (!input) {
+ return;
}
+ const start = input.selectionStart;
+ const end = input.selectionEnd;
+ const currentText = this.newMessageText || "";
+ this.newMessageText = currentText.substring(0, start) + text + currentText.substring(end);
+
+ this.$nextTick(() => {
+ input.focus();
+ const newCursorPos = start + text.length;
+ input.setSelectionRange(newCursorPos, newCursorPos);
+ input.style.height = "auto";
+ input.style.height = Math.min(input.scrollHeight, 200) + "px";
+ });
},
onFileInputChange: function (event) {
const selectedFiles = Array.from(event.target.files || []);
@@ -5422,6 +6162,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.attachment-card {
@apply relative flex gap-3 border border-gray-200 dark:border-zinc-800 rounded-2xl p-3 shadow-sm;
background-color: white;
@@ -5455,16 +6196,16 @@ export default {
@apply inline-flex items-center justify-center text-gray-500 dark:text-gray-300 hover:text-red-500;
}
.attachment-action-button {
- @apply inline-flex items-center gap-1 rounded-full border border-gray-200 px-3 py-1.5 text-xs font-bold text-gray-700 bg-white shadow-sm transition-all !important;
+ @apply inline-flex items-center gap-1 rounded-full border border-gray-200 px-3 py-1.5 text-xs font-bold text-gray-700 bg-white shadow-xs transition-all;
}
.attachment-action-button:hover {
- @apply bg-gray-50 text-gray-900 border-blue-400 !important;
+ @apply bg-gray-50 text-gray-900 border-blue-400;
}
.dark .attachment-action-button {
- @apply border-zinc-700 text-zinc-100 bg-zinc-900 !important;
+ @apply border-zinc-700 text-zinc-100 bg-zinc-900;
}
.dark .attachment-action-button:hover {
- @apply bg-zinc-800 text-white border-blue-500 !important;
+ @apply bg-zinc-800 text-white border-blue-500;
}
.compose-emoji-picker {
diff --git a/meshchatx/src/frontend/components/messages/InViewAnimatedImg.vue b/meshchatx/src/frontend/components/messages/InViewAnimatedImg.vue
index 1f78f1fb..ca3caad8 100644
--- a/meshchatx/src/frontend/components/messages/InViewAnimatedImg.vue
+++ b/meshchatx/src/frontend/components/messages/InViewAnimatedImg.vue
@@ -42,7 +42,7 @@ export default {
if (this.fitParent) {
return "absolute inset-0 bg-zinc-200/30 dark:bg-white/10";
}
- return "min-h-[8rem] w-full rounded-2xl bg-gray-100/90 dark:bg-zinc-800/60";
+ return "min-h-32 w-full rounded-2xl bg-gray-100/90 dark:bg-zinc-800/60";
},
},
mounted() {
diff --git a/meshchatx/src/frontend/components/messages/MessagesPage.vue b/meshchatx/src/frontend/components/messages/MessagesPage.vue
index 904c0005..14a5649e 100644
--- a/meshchatx/src/frontend/components/messages/MessagesPage.vue
+++ b/meshchatx/src/frontend/components/messages/MessagesPage.vue
@@ -66,7 +66,7 @@
<button
v-if="!isPopoutMode && !destinationHash"
type="button"
- class="sm:hidden fixed bottom-5 right-4 z-[65] flex h-14 w-14 items-center justify-center rounded-full bg-zinc-900 text-white shadow-lg ring-1 ring-white/10 transition active:scale-95 dark:bg-zinc-100 dark:text-zinc-900 dark:ring-zinc-800"
+ class="sm:hidden fixed bottom-5 right-4 z-65 flex h-14 w-14 items-center justify-center rounded-full bg-zinc-900 text-white shadow-lg ring-1 ring-white/10 transition active:scale-95 dark:bg-zinc-100 dark:text-zinc-900 dark:ring-zinc-800"
:title="$t('app.compose')"
@click="openMobileCompose"
>
@@ -75,7 +75,7 @@
<div
v-if="isMobileComposeModalOpen"
- class="fixed inset-0 z-[95] flex items-end justify-center sm:items-center p-0 sm:p-4 bg-black/50 backdrop-blur-sm sm:bg-black/50"
+ class="fixed inset-0 z-95 flex items-end justify-center sm:items-center p-0 sm:p-4 bg-black/50 backdrop-blur-xs sm:bg-black/50"
@click.self="isMobileComposeModalOpen = false"
>
<div
@@ -115,14 +115,14 @@
autocorrect="off"
spellcheck="false"
:placeholder="$t('messages.mobile_compose_destination_placeholder')"
- class="block w-full rounded-xl border-0 py-2.5 px-3 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-xl border-0 py-2.5 px-3 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@keydown.enter="submitMobileCompose"
/>
</div>
<div class="flex flex-col gap-2">
<button
type="button"
- class="w-full flex justify-center items-center gap-2 py-2.5 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:pointer-events-none"
+ class="w-full flex justify-center items-center gap-2 py-2.5 px-4 border border-transparent rounded-xl shadow-xs text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:pointer-events-none"
:disabled="!mobileComposeAddress.trim()"
@click="submitMobileCompose"
>
@@ -144,7 +144,7 @@
<!-- Ingest Paper Message Modal -->
<div
v-if="isIngestModalOpen"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="isIngestModalOpen = false"
>
<div class="w-full max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden">
@@ -176,7 +176,7 @@
v-model="ingestUri"
type="text"
placeholder="lxmf://... or lxma://..."
- class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
+ class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@keydown.enter="ingestPaperMessage"
/>
<button
@@ -200,7 +200,7 @@
</div>
<button
type="button"
- class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
+ class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-xl shadow-xs text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
:disabled="!ingestUri"
@click="ingestPaperMessage"
>
@@ -216,7 +216,7 @@
<div
v-if="isIngestScannerModalOpen"
- class="fixed inset-0 z-[120] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
+ class="fixed inset-0 z-120 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs"
@click.self="closeIngestScannerModal"
>
<div class="w-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
diff --git a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue
index 63722c21..3227563f 100644
--- a/meshchatx/src/frontend/components/messages/MessagesSidebar.vue
+++ b/meshchatx/src/frontend/components/messages/MessagesSidebar.vue
@@ -55,7 +55,7 @@
v-for="c in collapsedSidebarConversations"
:key="c.destination_hash"
type="button"
- class="shrink-0 p-0.5 rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
+ class="shrink-0 p-0.5 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-indigo-500"
:class="
selectedDestinationHash === c.destination_hash
? 'ring-2 ring-indigo-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950'
@@ -156,7 +156,7 @@
<div
v-if="folderMenu.show"
v-click-outside="{ handler: () => (folderMenu.show = false), capture: true }"
- class="absolute right-0 top-full mt-1 z-[60] min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
+ class="absolute right-0 top-full mt-1 z-60 min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
>
<button
type="button"
@@ -314,7 +314,7 @@
<input
type="checkbox"
:checked="allSelected"
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
@change="toggleSelectAll"
/>
<span class="text-xs font-semibold text-blue-700 dark:text-blue-400">
@@ -347,7 +347,7 @@
<div
v-if="moveMenu.show"
v-click-outside="{ handler: () => (moveMenu.show = false), capture: true }"
- class="absolute right-0 top-full mt-1 z-[60] min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
+ class="absolute right-0 top-full mt-1 z-60 min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
>
<button
type="button"
@@ -376,10 +376,10 @@
<div v-if="isLoading" class="w-full divide-y divide-gray-100 dark:divide-zinc-800">
<div v-for="i in 6" :key="i" class="p-3 animate-pulse">
<div class="flex gap-3">
- <div class="rounded bg-gray-200 dark:bg-zinc-800" :style="messageIconStyle"></div>
+ <div class="rounded-sm bg-gray-200 dark:bg-zinc-800" :style="messageIconStyle"></div>
<div class="flex-1 space-y-2 py-1">
- <div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded w-3/4"></div>
- <div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded w-1/2"></div>
+ <div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded-sm w-3/4"></div>
+ <div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded-sm w-1/2"></div>
</div>
</div>
</div>
@@ -425,7 +425,7 @@
<input
type="checkbox"
:checked="selectedHashes.has(conversation.destination_hash)"
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
@click.stop
@change="toggleSelectConversation(conversation.destination_hash)"
/>
@@ -441,7 +441,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
- class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
+ class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
@@ -477,9 +477,7 @@
>
{{ conversation.custom_display_name ?? conversation.display_name }}
</div>
- <div
- class="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap flex-shrink-0"
- >
+ <div class="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap shrink-0">
{{ formatTimeAgo(conversation.updated_at) }}
</div>
</div>
@@ -532,7 +530,7 @@
:show="contextMenu.show"
:x="contextMenu.x"
:y="contextMenu.y"
- panel-class="z-[100]"
+ panel-class="z-100"
>
<ContextMenuItem @click="bulkMarkAsRead">
<MaterialDesignIcon icon-name="email-open-outline" class="size-4 text-gray-400" />
@@ -695,7 +693,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
- class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
+ class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
diff --git a/meshchatx/src/frontend/components/messages/PaperMessageModal.vue b/meshchatx/src/frontend/components/messages/PaperMessageModal.vue
index 13822640..cd1adebb 100644
--- a/meshchatx/src/frontend/components/messages/PaperMessageModal.vue
+++ b/meshchatx/src/frontend/components/messages/PaperMessageModal.vue
@@ -2,7 +2,7 @@
<template>
<div
- class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity"
+ class="fixed inset-0 z-150 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs transition-opacity"
@click.self="close"
>
<div
@@ -66,7 +66,7 @@
</div>
<button
type="button"
- class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-sm"
+ class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-xs"
title="Copy URI"
@click="copyUri"
>
@@ -120,7 +120,7 @@
</div>
<button
type="button"
- class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-sm"
+ class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-xs"
title="Copy URI"
@click="copyUri"
>
diff --git a/meshchatx/src/frontend/components/messages/SendMessageButton.vue b/meshchatx/src/frontend/components/messages/SendMessageButton.vue
index 920db7ae..dfb4718e 100644
--- a/meshchatx/src/frontend/components/messages/SendMessageButton.vue
+++ b/meshchatx/src/frontend/components/messages/SendMessageButton.vue
@@ -1,12 +1,12 @@
<!-- SPDX-License-Identifier: 0BSD AND MIT -->
<template>
- <div class="relative inline-flex items-stretch rounded-xl shadow-sm">
+ <div class="relative inline-flex items-stretch rounded-xl shadow-xs">
<template v-if="compact">
<button
:disabled="!canSendMessage"
type="button"
- class="inline-flex items-center justify-center rounded-xl p-2.5 min-h-[44px] min-w-[44px] text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 touch-manipulation select-none"
+ class="inline-flex items-center justify-center rounded-xl p-2.5 min-h-[44px] min-w-[44px] text-white transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 touch-manipulation select-none"
:class="[
canSendMessage
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
@@ -40,7 +40,7 @@
<button
:disabled="!canSendMessage"
type="button"
- class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2.5 text-sm font-semibold text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
+ class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2.5 text-sm font-semibold text-white transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2"
:class="[
canSendMessage
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
@@ -75,7 +75,7 @@
<button
:disabled="!canSendMessage"
type="button"
- class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 h-full text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
+ class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 h-full text-white transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2"
:class="[
canSendMessage
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500 border-blue-700 dark:border-blue-800'
@@ -105,7 +105,7 @@
<div
v-if="isShowingMenu"
v-click-outside="hideMenu"
- class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none overflow-hidden min-w-[200px]"
+ class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-hidden overflow-hidden min-w-[200px]"
>
<div class="py-1">
<button
diff --git a/meshchatx/src/frontend/components/messages/messageListVirtual.js b/meshchatx/src/frontend/components/messages/messageListVirtual.js
index bbeca6d3..023c20eb 100644
--- a/meshchatx/src/frontend/components/messages/messageListVirtual.js
+++ b/meshchatx/src/frontend/components/messages/messageListVirtual.js
@@ -22,6 +22,9 @@ export function estimateGroupHeight(entry) {
if (!entry || typeof entry !== "object") {
return 96;
}
+ if (entry.type === "dateDivider") {
+ return 44;
+ }
if (entry.type === "imageGroup") {
return 340;
}
@@ -42,6 +45,9 @@ export function findDisplayGroupIndexForMessageHash(groupsOldestFirst, hash) {
if (!g || typeof g !== "object") {
continue;
}
+ if (g.type === "dateDivider") {
+ continue;
+ }
if (g.type === "imageGroup" && Array.isArray(g.items)) {
if (g.items.some((it) => it?.lxmf_message?.hash === hash)) {
return i;
diff --git a/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryListItem.vue b/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryListItem.vue
index ce210978..71f4a3b6 100644
--- a/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryListItem.vue
+++ b/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryListItem.vue
@@ -4,7 +4,7 @@
<div class="p-3 rounded-xl border border-gray-100 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-900/30">
<div class="flex justify-between items-start mb-2">
<span
- class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded bg-gray-200 dark:bg-zinc-800 text-gray-600 dark:text-zinc-400"
+ class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-sm bg-gray-200 dark:bg-zinc-800 text-gray-600 dark:text-zinc-400"
>
{{ item.is_outbound ? $t("messages.telemetry_label_sent") : $t("messages.telemetry_label_received") }}
</span>
diff --git a/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryModal.vue b/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryModal.vue
index f7f50327..9a8d5b01 100644
--- a/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryModal.vue
+++ b/meshchatx/src/frontend/components/messages/telemetry/TelemetryHistoryModal.vue
@@ -4,7 +4,7 @@
<Teleport to="body">
<div
v-if="modelValue"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="close"
>
<div
@@ -56,7 +56,7 @@
<input
:checked="showTelemetryInChat"
type="checkbox"
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
+ class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
@change="onShowTelemetryChange"
/>
<span
@@ -66,7 +66,7 @@
</label>
<button
type="button"
- class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-sm shrink-0"
+ class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-xs shrink-0"
@click="close"
>
{{ $t("messages.telemetry_history_done") }}
diff --git a/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue b/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue
index cc3fb120..04592f83 100644
--- a/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue
+++ b/meshchatx/src/frontend/components/micron-editor/MicronEditorPage.vue
@@ -4,7 +4,7 @@
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
<!-- Compact Header -->
<div
- class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-sm shrink-0 min-w-0"
+ class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-xs shrink-0 min-w-0"
>
<div class="flex items-center gap-2 sm:gap-3 min-w-0">
<div class="bg-teal-100 dark:bg-teal-900/30 p-1.5 rounded-xl shrink-0">
@@ -27,20 +27,27 @@
</div>
</div>
<div class="flex items-center gap-2">
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-teal-600 dark:text-teal-300 hover:underline shrink-0"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
<button
type="button"
- class="secondary-chip !py-1 !px-3 !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
+ class="secondary-chip py-1! px-3! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
@click="resetAll"
>
<MaterialDesignIcon icon-name="refresh" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">{{ $t("tools.micron_editor.reset") }}</span>
</button>
- <button type="button" class="secondary-chip !py-1 !px-3" @click="downloadFile">
+ <button type="button" class="secondary-chip py-1! px-3!" @click="downloadFile">
<MaterialDesignIcon icon-name="download" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">{{ $t("tools.micron_editor.save") }}</span>
</button>
<div class="relative">
- <button type="button" class="primary-chip !py-1 !px-3" @click="togglePublishMenu">
+ <button type="button" class="primary-chip py-1! px-3!" @click="togglePublishMenu">
<MaterialDesignIcon icon-name="publish" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">Publish</span>
</button>
@@ -82,7 +89,7 @@
</div>
</div>
</div>
- <button v-if="isMobileView" type="button" class="primary-chip !py-1 !px-3" @click="toggleView">
+ <button v-if="isMobileView" type="button" class="primary-chip py-1! px-3!" @click="toggleView">
<MaterialDesignIcon :icon-name="showEditor ? 'eye' : 'pencil'" class="w-3.5 h-3.5" />
{{ showEditor ? $t("tools.micron_editor.view_preview") : $t("tools.micron_editor.edit") }}
</button>
@@ -99,7 +106,7 @@
class="group flex items-center h-8 px-3 rounded-lg text-xs font-medium transition-colors cursor-pointer whitespace-nowrap"
:class="[
activeTabIndex === index
- ? 'bg-white dark:bg-zinc-800 text-teal-600 dark:text-teal-400 shadow-sm'
+ ? 'bg-white dark:bg-zinc-800 text-teal-600 dark:text-teal-400 shadow-xs'
: 'text-gray-500 hover:bg-white/50 dark:hover:bg-zinc-800/50 hover:text-gray-700 dark:hover:text-zinc-300',
]"
@click="activeTabIndex = index"
@@ -143,7 +150,7 @@
<textarea
ref="editorRef"
v-model="tabs[activeTabIndex].content"
- class="flex-1 w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-sm resize-none focus:outline-none"
+ class="flex-1 w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-sm resize-none focus:outline-hidden"
:placeholder="$t('tools.micron_editor.placeholder')"
@input="handleInput"
></textarea>
@@ -159,7 +166,7 @@
<!-- eslint-disable vue/no-v-html -->
<div
ref="previewRef"
- class="flex-1 overflow-auto text-zinc-100 p-4 font-mono text-sm whitespace-pre-wrap break-words nodeContainer"
+ class="flex-1 overflow-auto text-zinc-100 p-4 font-mono text-sm whitespace-pre-wrap wrap-break-word nodeContainer"
v-html="renderedContent"
></div>
<!-- eslint-enable vue/no-v-html -->
diff --git a/meshchatx/src/frontend/components/network-visualiser/internal/NetworkVisualiserToolbar.vue b/meshchatx/src/frontend/components/network-visualiser/internal/NetworkVisualiserToolbar.vue
index b4fe4bc7..0118a3fe 100644
--- a/meshchatx/src/frontend/components/network-visualiser/internal/NetworkVisualiserToolbar.vue
+++ b/meshchatx/src/frontend/components/network-visualiser/internal/NetworkVisualiserToolbar.vue
@@ -86,7 +86,7 @@
v-show="isShowingControls"
class="px-5 pb-5 space-y-4 animate-in fade-in slide-in-from-top-2 duration-300"
>
- <div class="h-px bg-gradient-to-r from-transparent via-gray-200 dark:via-zinc-800 to-transparent"></div>
+ <div class="h-px bg-linear-to-r from-transparent via-gray-200 dark:via-zinc-800 to-transparent"></div>
<div class="flex items-center justify-between">
<label
@@ -128,7 +128,7 @@
autocomplete="off"
maxlength="4"
:aria-label="$t('visualiser.max_hops_filter')"
- class="w-[3.25rem] shrink-0 rounded-lg border border-gray-200 bg-white px-1.5 py-1 text-center text-xs font-bold text-blue-600 tabular-nums shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500/40 dark:border-zinc-600 dark:bg-zinc-800 dark:text-blue-400 dark:focus:border-blue-500"
+ class="w-13 shrink-0 rounded-lg border border-gray-200 bg-white px-1.5 py-1 text-center text-xs font-bold text-blue-600 tabular-nums shadow-xs focus:border-blue-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500/40 dark:border-zinc-600 dark:bg-zinc-800 dark:text-blue-400 dark:focus:border-blue-500"
:value="hopMaxInputShown"
:placeholder="$t('visualiser.all')"
@focus="onHopMaxInputFocus"
@@ -213,7 +213,7 @@
:value="searchQuery"
type="text"
:placeholder="`Search nodes (${nodeCount})...`"
- class="block w-full sm:w-64 pl-9 pr-10 py-2.5 sm:py-3 bg-white/70 dark:bg-zinc-900/70 backdrop-blur-xl border border-gray-200/50 dark:border-zinc-800/50 rounded-2xl text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-blue-500/50 sm:focus:w-80 md:max-lg:focus:w-72 lg:focus:w-80 transition-all dark:text-zinc-100 shadow-sm"
+ class="block w-full sm:w-64 pl-9 pr-10 py-2.5 sm:py-3 bg-white/70 dark:bg-zinc-900/70 backdrop-blur-xl border border-gray-200/50 dark:border-zinc-800/50 rounded-2xl text-xs font-semibold focus:outline-hidden focus:ring-2 focus:ring-blue-500/50 sm:focus:w-80 md:max-lg:focus:w-72 lg:focus:w-80 transition-all dark:text-zinc-100 shadow-xs"
@input="$emit('update:searchQuery', $event.target.value)"
/>
<button
diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
index f43f863e..b3156f42 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkPage.vue
@@ -39,7 +39,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
- class="banished-text !opacity-100 !text-white !shadow-lg !bg-red-600 !px-4 !py-2 !rounded-xl !border-2 !tracking-widest"
+ class="banished-text opacity-100! text-white! shadow-lg! bg-red-600! px-4! py-2! rounded-xl! border-2! tracking-widest!"
:style="{
'background-color': GlobalState.config.banished_color,
'border-color': GlobalState.config.banished_color,
@@ -81,7 +81,7 @@
>
<span
v-if="selectedNodePath"
- class="text-sm cursor-pointer whitespace-nowrap flex-shrink-0 hidden sm:inline"
+ class="text-sm cursor-pointer whitespace-nowrap shrink-0 hidden sm:inline"
@click="onDestinationPathClick(selectedNodePath)"
>
- {{ selectedNodePath.hops }}
@@ -286,7 +286,7 @@
<!-- page content: capture-phase clicks so <a href> is handled before browser default navigation -->
<div
:class="[
- 'flex-1 overflow-y-auto nodeContainer relative [contain:layout_paint]',
+ 'flex-1 overflow-y-auto nodeContainer relative contain-[layout_paint]',
nomadRenderedShellFullBleed
? 'p-0 bg-transparent text-gray-900 dark:text-gray-100'
: 'p-3 bg-black text-white',
@@ -297,7 +297,7 @@
<div
v-if="isShowingArchivedVersion"
:class="[
- 'mb-4 p-2 bg-yellow-900/40 border border-yellow-700/50 rounded flex items-center justify-between text-yellow-200',
+ 'mb-4 p-2 bg-yellow-900/40 border border-yellow-700/50 rounded-sm flex items-center justify-between text-yellow-200',
nomadRenderedShellFullBleed ? 'mx-3 mt-3' : '',
]"
>
@@ -311,7 +311,7 @@
}}</span>
</div>
<button
- class="text-xs bg-yellow-700/50 hover:bg-yellow-700 px-2 py-1 rounded transition"
+ class="text-xs bg-yellow-700/50 hover:bg-yellow-700 px-2 py-1 rounded-sm transition"
@click="reloadNodePage"
>
{{ $t("nomadnet.load_live") }}
@@ -344,7 +344,7 @@
<div class="my-auto flex-1">{{ nomadnetPageLoadingLine }}</div>
<button
type="button"
- class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded px-3 py-1 text-sm font-semibold cursor-pointer ml-3"
+ class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded-sm px-3 py-1 text-sm font-semibold cursor-pointer ml-3"
@click="cancelPageDownload"
>
{{ $t("common.cancel") }}
@@ -360,7 +360,7 @@
<div v-if="hasArchivesForCurrentPage" class="space-y-2">
<div class="text-sm text-gray-300">{{ $t("nomadnet.archived_version_available") }}</div>
<button
- class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-500 transition"
+ class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition"
@click="toggleArchiveDropdown"
>
<MaterialDesignIcon icon-name="archive" class="size-4" />
@@ -413,7 +413,7 @@
</div>
<button
type="button"
- class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded px-3 py-1 text-sm font-semibold cursor-pointer"
+ class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded-sm px-3 py-1 text-sm font-semibold cursor-pointer"
@click="cancelFileDownload"
>
{{ $t("common.cancel") }}
@@ -431,7 +431,7 @@
<div class="mx-auto mt-2">
<button
type="button"
- class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
+ class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-xs hover:bg-gray-400 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
@click.stop="openUrl"
>
{{ $t("nomadnet.open_nomadnet_url") }}
@@ -452,6 +452,7 @@ import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
import Utils from "../../js/Utils";
import DownloadUtils from "../../js/DownloadUtils";
import ToastUtils from "../../js/ToastUtils";
+import { getDestinationPath, runDestinationPathFinder } from "../../js/reticulumPathfinding.js";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import IconButton from "../IconButton.vue";
import DropDownMenu from "../DropDownMenu.vue";
@@ -620,14 +621,14 @@ export default {
},
nomadPageContentClasses() {
if (!this.nodePagePath || this.isShowingNodePageSource) {
- return ["h-full", "break-words", "whitespace-pre-wrap", "text-gray-100"];
+ return ["h-full", "wrap-break-word", "whitespace-pre-wrap", "text-gray-100"];
}
const [p] = this.nodePagePath.split("`");
const pl = (p || "").toLowerCase();
const isRich = pl.endsWith(".mu") || pl.endsWith(".md") || pl.endsWith(".html");
const isHtml = pl.endsWith(".html");
const isMd = pl.endsWith(".md");
- const classes = ["h-full", "break-words"];
+ const classes = ["h-full", "wrap-break-word"];
if (this.nomadRenderedShellFullBleed && !isHtml) {
classes.push("px-3", "py-3");
}
@@ -1463,7 +1464,7 @@ export default {
if (!hash || this.pathfinderInProgress) return;
this.pathfinderInProgress = true;
try {
- await window.api.post(`/api/v1/destination/${hash}/request-path`);
+ await runDestinationPathFinder(window.api, hash, "quick");
ToastUtils.success(this.$t("nomadnet.path_finder_request_sent"));
await this.reloadNodePage();
} catch (e) {
@@ -1478,10 +1479,10 @@ export default {
if (!hash || this.pathfinderInProgress) return;
this.pathfinderInProgress = true;
try {
- const response = await window.api.get(`/api/v1/destination/${hash}/path`, {
- params: { request: "1", timeout: 15 },
+ const { path } = await runDestinationPathFinder(window.api, hash, "force", {
+ forceTimeout: 15,
});
- if (response?.data?.path) {
+ if (path) {
ToastUtils.success(this.$t("nomadnet.path_finder_found"));
await this.reloadNodePage();
} else {
@@ -1499,12 +1500,9 @@ export default {
if (!hash || this.pathfinderInProgress) return;
this.pathfinderInProgress = true;
try {
- try {
- await window.api.post(`/api/v1/destination/${hash}/drop-path`);
- } catch (e) {
- console.warn("drop-path failed (continuing)", e);
- }
- await window.api.post(`/api/v1/destination/${hash}/request-path`);
+ await runDestinationPathFinder(window.api, hash, "drop_then_request", {
+ onDropPathError: (e) => console.warn("drop-path failed (continuing)", e),
+ });
ToastUtils.success(this.$t("nomadnet.path_finder_dropped_and_requested"));
await this.reloadNodePage();
} catch (e) {
@@ -1908,14 +1906,11 @@ export default {
return `${m}m ${rs}s`;
},
async getNodePath(destinationHash) {
- // clear previous known path
this.selectedNodePath = null;
try {
- // get path to destination
- const response = await window.api.get(`/api/v1/destination/${destinationHash}/path`);
+ const response = await getDestinationPath(window.api, destinationHash, {});
- // update ui
this.selectedNodePath = response.data.path;
} catch (e) {
console.log(e);
diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue
index 507d9d0b..340ffecd 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue
@@ -51,7 +51,7 @@
v-for="fav in collapsedFavouritePreview"
:key="fav.destination_hash"
type="button"
- class="shrink-0 p-1 rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
+ class="shrink-0 p-1 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
:class="
fav.destination_hash === selectedDestinationHash
? 'ring-2 ring-blue-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950'
@@ -71,7 +71,7 @@
v-for="node in collapsedAnnounceNodesPreview"
:key="node.destination_hash"
type="button"
- class="shrink-0 p-1 rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
+ class="shrink-0 p-1 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
:class="
node.destination_hash === selectedDestinationHash
? 'ring-2 ring-blue-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950'
@@ -182,7 +182,7 @@
:ref="`sectionInput-${section.id}`"
v-model="editingSectionName"
type="text"
- class="flex-1 bg-transparent border-b border-blue-500 text-xs font-semibold uppercase tracking-wide text-gray-900 dark:text-white focus:outline-none min-w-0"
+ class="flex-1 bg-transparent border-b border-blue-500 text-xs font-semibold uppercase tracking-wide text-gray-900 dark:text-white focus:outline-hidden min-w-0"
@click.stop
@keydown.enter="saveSectionName"
@keydown.esc="cancelEditingSection"
@@ -242,13 +242,13 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
- class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
+ class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
</div>
- <div class="favourite-card__icon flex-shrink-0">
+ <div class="favourite-card__icon shrink-0">
<MaterialDesignIcon icon-name="server-network" class="w-5 h-5" />
</div>
<div class="min-w-0 flex-1">
@@ -267,7 +267,7 @@
</div>
</div>
<IconButton
- class="flex-shrink-0 text-gray-500 dark:text-gray-300"
+ class="shrink-0 text-gray-500 dark:text-gray-300"
@click.stop="openFavouriteContextMenu($event, favourite, section.id)"
>
<MaterialDesignIcon icon-name="dots-vertical" class="w-5 h-5" />
@@ -301,7 +301,7 @@
:show="favouriteContextMenu.show"
:x="favouriteContextMenu.x"
:y="favouriteContextMenu.y"
- panel-class="z-[200] min-w-56"
+ panel-class="z-200 min-w-56"
>
<ContextMenuItem @click="renameFavouriteFromContext">
<MaterialDesignIcon icon-name="pencil" class="size-4 text-gray-400" />
@@ -352,7 +352,7 @@
:show="sectionContextMenu.show"
:x="sectionContextMenu.x"
:y="sectionContextMenu.y"
- panel-class="z-[200]"
+ panel-class="z-200"
>
<ContextMenuItem @click="renameSectionFromContext">
<MaterialDesignIcon icon-name="pencil" class="size-4 text-gray-400" />
@@ -405,7 +405,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
- class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
+ class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
@@ -415,7 +415,7 @@
class="flex items-center gap-3 flex-1 min-w-0 cursor-pointer"
@click="onNodeClick(node)"
>
- <div class="announce-card__icon flex-shrink-0">
+ <div class="announce-card__icon shrink-0">
<MaterialDesignIcon icon-name="satellite-uplink" class="w-5 h-5" />
</div>
<div class="min-w-0 flex-1">
@@ -441,7 +441,7 @@
</div>
</div>
</div>
- <div class="flex-shrink-0">
+ <div class="shrink-0">
<DropDownMenu>
<template #button>
<IconButton>
@@ -494,7 +494,7 @@
:show="announceContextMenu.show"
:x="announceContextMenu.x"
:y="announceContextMenu.y"
- panel-class="z-[200]"
+ panel-class="z-200"
>
<ContextMenuItem
v-if="!isFavourite(announceContextMenu.node?.destination_hash)"
@@ -1304,6 +1304,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.sidebar-tab {
@apply flex h-full w-1/2 items-center justify-center text-sm font-semibold text-gray-500 dark:text-gray-400 border-b-2 border-transparent transition;
}
diff --git a/meshchatx/src/frontend/components/page-nodes/PageNodesPage.vue b/meshchatx/src/frontend/components/page-nodes/PageNodesPage.vue
index c44a1b11..99196240 100644
--- a/meshchatx/src/frontend/components/page-nodes/PageNodesPage.vue
+++ b/meshchatx/src/frontend/components/page-nodes/PageNodesPage.vue
@@ -70,35 +70,35 @@
</span>
<button
v-if="!node.running"
- class="primary-chip !py-1 !px-3 !text-xs"
+ class="primary-chip py-1! px-3! text-xs!"
@click.stop="startNode(node.node_id)"
>
Start
</button>
<button
v-else
- class="secondary-chip !py-1 !px-3 !text-xs !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
+ class="secondary-chip py-1! px-3! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
@click.stop="stopNode(node.node_id)"
>
Stop
</button>
<button
v-if="node.running"
- class="secondary-chip !py-1 !px-3 !text-xs"
+ class="secondary-chip py-1! px-3! text-xs!"
@click.stop="announceNode(node.node_id)"
>
Announce
</button>
<button
v-if="node.running && node.destination_hash"
- class="secondary-chip !py-1 !px-3 !text-xs"
+ class="secondary-chip py-1! px-3! text-xs!"
@click.stop="viewNode(node)"
>
<MaterialDesignIcon icon-name="eye" class="w-3.5 h-3.5" />
View
</button>
<button
- class="secondary-chip !py-1 !px-3 !text-xs !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
+ class="secondary-chip py-1! px-3! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
@click.stop="deleteNode(node.node_id)"
>
<MaterialDesignIcon icon-name="delete" class="w-3.5 h-3.5" />
@@ -126,10 +126,10 @@
{{ selectedNode.name }}
</div>
<div class="flex items-center gap-2">
- <button class="secondary-chip !py-1 !px-3 !text-xs" @click="showRenameDialog = true">
+ <button class="secondary-chip py-1! px-3! text-xs!" @click="showRenameDialog = true">
Rename
</button>
- <button class="secondary-chip !py-1 !px-3 !text-xs" @click="selectedNode = null">
+ <button class="secondary-chip py-1! px-3! text-xs!" @click="selectedNode = null">
<MaterialDesignIcon icon-name="close" class="w-3.5 h-3.5" />
</button>
</div>
@@ -143,7 +143,7 @@
<div class="text-xs font-bold uppercase tracking-wider">Destination Hash</div>
<button
v-if="selectedNode.running"
- class="primary-chip !py-0.5 !px-2 !text-xs"
+ class="primary-chip py-0.5! px-2! text-xs!"
@click="viewNode(selectedNode)"
>
<MaterialDesignIcon icon-name="eye" class="w-3 h-3" />
@@ -189,7 +189,7 @@
class="input-field flex-1"
@keyup.enter="addPage"
/>
- <button class="primary-chip !py-1 !px-3 !text-xs" @click="addPage">
+ <button class="primary-chip py-1! px-3! text-xs!" @click="addPage">
<MaterialDesignIcon icon-name="plus" class="w-3.5 h-3.5" />
Add Page
</button>
@@ -212,11 +212,11 @@
<span class="text-sm font-mono text-gray-900 dark:text-white">{{ page }}</span>
</div>
<div class="flex items-center gap-2">
- <button class="secondary-chip !py-0.5 !px-2 !text-xs" @click="editPage(page)">
+ <button class="secondary-chip py-0.5! px-2! text-xs!" @click="editPage(page)">
Edit
</button>
<button
- class="secondary-chip !py-0.5 !px-2 !text-xs !text-red-500"
+ class="secondary-chip py-0.5! px-2! text-xs! text-red-500!"
@click="deletePage(page)"
>
<MaterialDesignIcon icon-name="delete" class="w-3 h-3" />
@@ -231,15 +231,15 @@
Editing: {{ editingPage }}
</div>
<div class="flex gap-2">
- <button class="primary-chip !py-1 !px-3 !text-xs" @click="savePage">Save</button>
- <button class="secondary-chip !py-1 !px-3 !text-xs" @click="editingPage = null">
+ <button class="primary-chip py-1! px-3! text-xs!" @click="savePage">Save</button>
+ <button class="secondary-chip py-1! px-3! text-xs!" @click="editingPage = null">
Cancel
</button>
</div>
</div>
<textarea
v-model="editingPageContent"
- class="w-full h-64 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-3 font-mono text-sm rounded-lg border border-gray-200 dark:border-zinc-700 resize-y focus:outline-none focus:ring-2 focus:ring-blue-500/50"
+ class="w-full h-64 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-3 font-mono text-sm rounded-lg border border-gray-200 dark:border-zinc-700 resize-y focus:outline-hidden focus:ring-2 focus:ring-blue-500/50"
></textarea>
</div>
</div>
@@ -248,7 +248,7 @@
<div v-if="detailTab === 'files'" class="space-y-3">
<div class="flex gap-2">
<input ref="fileInput" type="file" class="hidden" @change="uploadFile" />
- <button class="primary-chip !py-1 !px-3 !text-xs" @click="$refs.fileInput.click()">
+ <button class="primary-chip py-1! px-3! text-xs!" @click="$refs.fileInput.click()">
<MaterialDesignIcon icon-name="upload" class="w-3.5 h-3.5" />
Upload File
</button>
@@ -274,7 +274,7 @@
}}</span>
</div>
<button
- class="secondary-chip !py-0.5 !px-2 !text-xs !text-red-500"
+ class="secondary-chip py-0.5! px-2! text-xs! text-red-500!"
@click="deleteFile(file.name)"
>
<MaterialDesignIcon icon-name="delete" class="w-3 h-3" />
@@ -304,10 +304,10 @@
/>
</div>
<div class="flex justify-end gap-2">
- <button class="secondary-chip !py-1.5 !px-4 !text-sm" @click="showCreateDialog = false">
+ <button class="secondary-chip py-1.5! px-4! text-sm!" @click="showCreateDialog = false">
Cancel
</button>
- <button class="primary-chip !py-1.5 !px-4 !text-sm" @click="createNode">Create</button>
+ <button class="primary-chip py-1.5! px-4! text-sm!" @click="createNode">Create</button>
</div>
</div>
</div>
@@ -331,10 +331,10 @@
/>
</div>
<div class="flex justify-end gap-2">
- <button class="secondary-chip !py-1.5 !px-4 !text-sm" @click="showRenameDialog = false">
+ <button class="secondary-chip py-1.5! px-4! text-sm!" @click="showRenameDialog = false">
Cancel
</button>
- <button class="primary-chip !py-1.5 !px-4 !text-sm" @click="renameNode">Rename</button>
+ <button class="primary-chip py-1.5! px-4! text-sm!" @click="renameNode">Rename</button>
</div>
</div>
</div>
diff --git a/meshchatx/src/frontend/components/ping/PingPage.vue b/meshchatx/src/frontend/components/ping/PingPage.vue
index 9a2daed1..b01e41c3 100644
--- a/meshchatx/src/frontend/components/ping/PingPage.vue
+++ b/meshchatx/src/frontend/components/ping/PingPage.vue
@@ -49,7 +49,7 @@
<button
v-else
type="button"
- class="secondary-chip !text-red-600 dark:!text-red-300 !border-red-200 dark:!border-red-500/50"
+ class="secondary-chip text-red-600! dark:text-red-300! border-red-200! dark:border-red-500/50!"
@click="stop"
>
<MaterialDesignIcon icon-name="pause" class="w-4 h-4" />
diff --git a/meshchatx/src/frontend/components/profile/ProfileIconPage.vue b/meshchatx/src/frontend/components/profile/ProfileIconPage.vue
index de45ce35..d8a54c1c 100644
--- a/meshchatx/src/frontend/components/profile/ProfileIconPage.vue
+++ b/meshchatx/src/frontend/components/profile/ProfileIconPage.vue
@@ -5,7 +5,7 @@
<div class="overflow-y-auto">
<div class="max-w-4xl mx-auto p-4 space-y-6">
<!-- Header with Preview -->
- <div class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800">
+ <div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800">
<div class="p-6 border-b border-gray-200 dark:border-zinc-800">
<div class="flex items-center justify-between">
<div>
@@ -66,7 +66,7 @@
</div>
<!-- Color Selection -->
- <div class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800">
+ <div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800">
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Colors</h3>
</div>
@@ -112,7 +112,7 @@
<!-- Icon Selection -->
<div
- class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
+ class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800 overflow-hidden"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Icon</h3>
@@ -183,7 +183,7 @@
<!-- Remove Icon Section -->
<div
- class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
+ class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800 overflow-hidden"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Remove Icon</h3>
diff --git a/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue b/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue
index e38b4dad..fa427da4 100644
--- a/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue
+++ b/meshchatx/src/frontend/components/propagation-nodes/PropagationNodesPage.vue
@@ -237,7 +237,7 @@
<div class="flex flex-wrap gap-2">
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors disabled:opacity-40"
+ class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors disabled:opacity-40"
:disabled="!localPropagationNode"
@click="useLocalPropagationNode"
>
@@ -257,11 +257,11 @@
v-model="searchTerm"
type="text"
:placeholder="`Search ${propagationNodes.length} Propagation Nodes...`"
- class="flex-1 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
+ class="flex-1 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-xs transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
/>
<select
v-model="sortBy"
- class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-sm transition-all min-w-[180px]"
+ class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-xs transition-all min-w-[180px]"
>
<option value="name">Sort by Name</option>
<option value="name-desc">Sort by Name (Z-A)</option>
@@ -277,7 +277,7 @@
<div
v-for="propagationNode of paginatedNodes"
:key="propagationNode.destination_hash"
- class="border border-gray-200 dark:border-zinc-800 rounded-2xl bg-white dark:bg-zinc-900 shadow-sm hover:shadow-md transition-shadow overflow-hidden"
+ class="border border-gray-200 dark:border-zinc-800 rounded-2xl bg-white dark:bg-zinc-900 shadow-xs hover:shadow-md transition-shadow overflow-hidden"
:class="{
'ring-2 ring-blue-500 dark:ring-blue-400':
config.lxmf_preferred_propagation_node_destination_hash ===
@@ -360,14 +360,14 @@
<span>TX {{ formatByteSize(propagationNode.local_node_stats.tx_bytes) }}</span>
</div>
</div>
- <div class="flex-shrink-0">
+ <div class="shrink-0">
<button
v-if="
config.lxmf_preferred_propagation_node_destination_hash ===
propagationNode.destination_hash
"
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-xl bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
+ class="inline-flex items-center gap-x-1.5 rounded-xl bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
@click="stopUsingPropagationNode"
>
Stop Using
@@ -375,7 +375,7 @@
<button
v-else
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
+ class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
@click="usePropagationNode(propagationNode.destination_hash)"
>
Set as Preferred
@@ -397,7 +397,7 @@
<button
:disabled="currentPage === 1"
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-sm transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-xs transition-colors"
@click="currentPage = Math.max(1, currentPage - 1)"
>
<svg
@@ -422,7 +422,7 @@
? 'bg-blue-600 text-white dark:bg-blue-600'
: 'bg-white dark:bg-zinc-900 text-gray-700 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-800',
]"
- class="w-10 h-10 rounded-xl border border-gray-200 dark:border-zinc-800 text-sm font-medium shadow-sm transition-colors"
+ class="w-10 h-10 rounded-xl border border-gray-200 dark:border-zinc-800 text-sm font-medium shadow-xs transition-colors"
@click="currentPage = page"
>
{{ page }}
@@ -431,7 +431,7 @@
<button
:disabled="currentPage === totalPages"
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-sm transition-colors"
+ class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-xs transition-colors"
@click="currentPage = Math.min(totalPages, currentPage + 1)"
>
Next
@@ -474,7 +474,7 @@
<div class="mt-4">
<button
type="button"
- class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
+ class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
@click="loadPropagationNodes"
>
Reload
@@ -513,6 +513,7 @@
import Utils from "../../js/Utils";
import WebSocketConnection from "../../js/WebSocketConnection";
import ToastUtils from "../../js/ToastUtils";
+import { getDestinationPath } from "../../js/reticulumPathfinding.js";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import {
incomingDeliveryBytesFromCustom,
@@ -855,8 +856,9 @@ export default {
return;
}
try {
- const response = await window.api.get(`/api/v1/destination/${hash}/path`, {
- params: { request: "1", timeout: 4 },
+ const response = await getDestinationPath(window.api, hash, {
+ request: "1",
+ timeout: 4,
});
this.nodePathsByHash = {
...this.nodePathsByHash,
diff --git a/meshchatx/src/frontend/components/rncp/RNCPPage.vue b/meshchatx/src/frontend/components/rncp/RNCPPage.vue
index a108337d..a6feb4af 100644
--- a/meshchatx/src/frontend/components/rncp/RNCPPage.vue
+++ b/meshchatx/src/frontend/components/rncp/RNCPPage.vue
@@ -136,7 +136,7 @@
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 cursor-pointer">
- <input v-model="sendNoCompress" type="checkbox" class="rounded" />
+ <input v-model="sendNoCompress" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.disable_compression")
}}</span>
@@ -251,7 +251,7 @@
</div>
<div class="flex items-center gap-2">
<label class="flex items-center gap-2 cursor-pointer">
- <input v-model="fetchAllowOverwrite" type="checkbox" class="rounded" />
+ <input v-model="fetchAllowOverwrite" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.allow_overwrite")
}}</span>
@@ -341,13 +341,13 @@
</div>
<div class="flex items-end gap-4">
<label class="flex items-center gap-2 cursor-pointer">
- <input v-model="listenFetchAllowed" type="checkbox" class="rounded" />
+ <input v-model="listenFetchAllowed" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.allow_fetch")
}}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
- <input v-model="listenAllowOverwrite" type="checkbox" class="rounded" />
+ <input v-model="listenAllowOverwrite" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.allow_overwrite")
}}</span>
diff --git a/meshchatx/src/frontend/components/rnode/RNodeAdvancedTools.vue b/meshchatx/src/frontend/components/rnode/RNodeAdvancedTools.vue
index c08fa687..884a4ce5 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeAdvancedTools.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeAdvancedTools.vue
@@ -57,6 +57,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.rnf-action-btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-xl bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 px-3 py-2.5 text-[11px] font-bold text-gray-700 dark:text-zinc-300 border border-gray-200 dark:border-zinc-700 transition-all active:scale-95;
}
diff --git a/meshchatx/src/frontend/components/rnode/RNodeBluetoothPanel.vue b/meshchatx/src/frontend/components/rnode/RNodeBluetoothPanel.vue
index fdf6ca13..46068182 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeBluetoothPanel.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeBluetoothPanel.vue
@@ -21,7 +21,7 @@
{{ $t("tools.rnode_flasher.disable") }}
</button>
<button
- class="rnf-action-btn col-span-2 sm:col-span-1 bg-blue-500 !text-white !border-none hover:bg-blue-600"
+ class="rnf-action-btn col-span-2 sm:col-span-1 bg-blue-500 text-white! border-none! hover:bg-blue-600"
@click="$emit('action', 'pair-bluetooth')"
>
<MaterialDesignIcon icon-name="key-link" class="size-4" />
@@ -46,6 +46,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.rnf-action-btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-xl bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 px-3 py-2.5 text-[11px] font-bold text-gray-700 dark:text-zinc-300 border border-gray-200 dark:border-zinc-700 transition-all active:scale-95;
}
diff --git a/meshchatx/src/frontend/components/rnode/RNodeDeviceSelector.vue b/meshchatx/src/frontend/components/rnode/RNodeDeviceSelector.vue
index 380ed736..1ca525d1 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeDeviceSelector.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeDeviceSelector.vue
@@ -164,6 +164,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.rnf-label {
@apply text-xs font-semibold text-gray-500 dark:text-zinc-500 uppercase tracking-wider;
}
diff --git a/meshchatx/src/frontend/components/rnode/RNodeFirmwareSelector.vue b/meshchatx/src/frontend/components/rnode/RNodeFirmwareSelector.vue
index 813926f4..d9d0f05b 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeFirmwareSelector.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeFirmwareSelector.vue
@@ -55,7 +55,7 @@
type="file"
accept=".zip"
data-testid="rnode-firmware-file"
- class="block w-full text-sm text-gray-900 dark:text-zinc-100 border border-gray-200 dark:border-zinc-800 rounded-xl cursor-pointer bg-white dark:bg-zinc-900 focus:outline-none file:mr-4 file:py-2.5 file:px-4 file:border-0 file:text-sm file:font-bold file:bg-zinc-200 dark:file:bg-zinc-700 file:text-zinc-700 dark:file:text-zinc-200 hover:file:bg-zinc-300 dark:hover:file:bg-zinc-600"
+ class="block w-full text-sm text-gray-900 dark:text-zinc-100 border border-gray-200 dark:border-zinc-800 rounded-xl cursor-pointer bg-white dark:bg-zinc-900 focus:outline-hidden file:mr-4 file:py-2.5 file:px-4 file:border-0 file:text-sm file:font-bold file:bg-zinc-200 dark:file:bg-zinc-700 file:text-zinc-700 dark:file:text-zinc-200 hover:file:bg-zinc-300 dark:hover:file:bg-zinc-600"
@change="onFileChange"
/>
</div>
@@ -121,6 +121,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.rnf-label {
@apply text-xs font-semibold text-gray-500 dark:text-zinc-500 uppercase tracking-wider;
}
diff --git a/meshchatx/src/frontend/components/rnode/RNodeFlashAction.vue b/meshchatx/src/frontend/components/rnode/RNodeFlashAction.vue
index 7e52524d..8987e128 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeFlashAction.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeFlashAction.vue
@@ -11,7 +11,7 @@
icon-name="alert-circle"
class="size-4 mt-0.5 text-red-600 dark:text-red-400 shrink-0"
/>
- <span class="text-xs text-red-600 dark:text-red-400 break-words">{{ errorMessage }}</span>
+ <span class="text-xs text-red-600 dark:text-red-400 wrap-break-word">{{ errorMessage }}</span>
</div>
<button
diff --git a/meshchatx/src/frontend/components/rnode/RNodeTncPanel.vue b/meshchatx/src/frontend/components/rnode/RNodeTncPanel.vue
index 3fddcd7b..190233dd 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeTncPanel.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeTncPanel.vue
@@ -55,7 +55,7 @@
</div>
<div class="grid grid-cols-2 gap-2">
<button
- class="rnf-action-btn bg-green-600 !text-white !border-none hover:bg-green-700"
+ class="rnf-action-btn bg-green-600 text-white! border-none! hover:bg-green-700"
@click="$emit('action', 'enable-tnc')"
>
{{ $t("tools.rnode_flasher.enable") }}
@@ -94,6 +94,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.rnf-input-label {
@apply text-[10px] font-bold text-zinc-400 uppercase tracking-widest;
}
diff --git a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
index 81f21eed..43bb6047 100644
--- a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
+++ b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
@@ -34,12 +34,12 @@
Refresh
</button>
<label class="secondary-chip inline-flex cursor-pointer items-center gap-2 px-4 py-2 text-sm">
- <input v-model="includeLinkStats" type="checkbox" class="rounded" />
+ <input v-model="includeLinkStats" type="checkbox" class="rounded-sm" />
<span>Include Link Stats</span>
</label>
<div class="flex min-w-0 flex-wrap items-center gap-2">
<label class="shrink-0 text-sm text-gray-700 dark:text-gray-300">Sort by:</label>
- <select v-model="sorting" class="input-field min-w-[10rem] text-sm">
+ <select v-model="sorting" class="input-field min-w-40 text-sm">
<option value="">None</option>
<option value="bitrate">Bitrate</option>
<option value="rx">RX Bytes</option>
@@ -81,7 +81,7 @@
<div
v-for="source in blackholeSources"
:key="source"
- class="text-sm font-mono bg-gray-50 dark:bg-gray-800 p-2 rounded truncate"
+ class="text-sm font-mono bg-gray-50 dark:bg-gray-800 p-2 rounded-sm truncate"
>
{{ source }}
</div>
@@ -106,7 +106,7 @@
<div class="min-w-0 flex-1 space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h3
- class="break-words text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
+ class="wrap-break-word text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
>
{{ iface.name }}
</h3>
diff --git a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
index 9ac1a9a6..6d614788 100644
--- a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
+++ b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
@@ -2,7 +2,7 @@
<template>
<div
- class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
+ class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
<div class="flex-1 overflow-y-auto w-full px-4 md:px-5 lg:px-8 py-6">
<div class="space-y-6 w-full max-w-4xl mx-auto">
@@ -19,7 +19,7 @@
<div class="flex flex-row gap-2 sm:flex-wrap sm:items-stretch sm:justify-end">
<button
type="button"
- class="inline-flex items-center justify-center gap-x-2 rounded-xl bg-blue-600 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all active:scale-[0.98] sm:rounded-2xl"
+ class="inline-flex items-center justify-center gap-x-2 rounded-xl bg-blue-600 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-blue-500 transition-all active:scale-[0.98] sm:rounded-2xl"
:title="$t('identities.new_identity')"
@click="showCreateModal = true"
>
@@ -65,8 +65,8 @@
>
<div class="w-14 h-14 rounded-2xl bg-gray-200 dark:bg-zinc-700 animate-pulse shrink-0" />
<div class="flex-1 min-w-0 space-y-2">
- <div class="h-5 w-32 bg-gray-200 dark:bg-zinc-700 rounded animate-pulse" />
- <div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded animate-pulse" />
+ <div class="h-5 w-32 bg-gray-200 dark:bg-zinc-700 rounded-sm animate-pulse" />
+ <div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded-sm animate-pulse" />
</div>
</div>
</template>
@@ -99,9 +99,9 @@
class="w-12 h-12 sm:w-14 sm:h-14 rounded-2xl flex items-center justify-center shadow-inner overflow-hidden transition-all duration-500"
:class="
identity.is_current && !identity.icon_background_colour
- ? 'bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/50 dark:to-indigo-900/50'
+ ? 'bg-linear-to-br from-blue-100 to-indigo-100 dark:from-blue-900/50 dark:to-indigo-900/50'
: !identity.icon_background_colour
- ? 'bg-gradient-to-br from-gray-100 to-slate-100 dark:from-zinc-800 dark:to-zinc-800/50'
+ ? 'bg-linear-to-br from-gray-100 to-slate-100 dark:from-zinc-800 dark:to-zinc-800/50'
: ''
"
:style="
@@ -129,14 +129,14 @@
</div>
<div
v-if="identity.is_current"
- class="absolute -top-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-zinc-900 shadow-sm"
+ class="absolute -top-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-zinc-900 shadow-xs"
></div>
</div>
<!-- info -->
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2">
- <h3 class="font-bold text-gray-900 dark:text-white break-words sm:truncate">
+ <h3 class="font-bold text-gray-900 dark:text-white wrap-break-word sm:truncate">
{{ identity.display_name }}
</h3>
<span
@@ -242,7 +242,7 @@
<!-- create modal -->
<div
v-if="showCreateModal"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
>
<div class="glass-card w-full max-w-md shadow-2xl animate-in fade-in zoom-in duration-200">
<div class="p-6">
@@ -292,7 +292,7 @@
<!-- import modal -->
<div
v-if="showImportModal"
- class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
+ class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
@click.self="showImportModal = false"
>
<div class="glass-card w-full max-w-md shadow-2xl animate-in fade-in zoom-in duration-200">
@@ -605,8 +605,9 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.glass-card {
- @apply bg-white/90 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg;
+ @apply bg-white/90 dark:bg-zinc-900/80 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg;
}
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-3 text-gray-900 dark:text-gray-100 transition;
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index 56e14589..db407687 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -3,10 +3,10 @@
<template>
<div
v-if="config"
- class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
+ class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full min-w-0 px-3 sm:px-5 md:px-5 lg:px-8 py-4 sm:py-6">
- <div class="space-y-0 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto min-w-0">
+ <div class="space-y-0 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-360 mx-auto min-w-0">
<div class="settings-section settings-section--hero">
<div class="flex flex-col lg:flex-row lg:items-center gap-4">
<div class="flex-1 space-y-1">
@@ -19,7 +19,7 @@
v-model="config.display_name"
type="text"
:placeholder="$t('app.display_name_placeholder')"
- class="w-full rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-base font-semibold text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500/60 focus:border-blue-500 outline-none transition"
+ class="w-full rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-base font-semibold text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500/60 focus:border-blue-500 outline-hidden transition"
@input="onDisplayNameChange"
/>
</div>
@@ -42,7 +42,7 @@
class="grid grid-cols-1 sm:grid-cols-3 gap-2 sm:gap-3 mt-4 text-sm text-gray-600 dark:text-gray-300"
>
<div
- class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
+ class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2"
>
<div class="text-xs uppercase tracking-wide">{{ $t("app.theme") }}</div>
<div class="font-semibold text-gray-900 dark:text-white capitalize">
@@ -50,7 +50,7 @@
</div>
</div>
<div
- class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
+ class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2"
>
<div class="text-xs uppercase tracking-wide">{{ $t("app.transport") }}</div>
<div class="font-semibold text-gray-900 dark:text-white">
@@ -58,7 +58,7 @@
</div>
</div>
<div
- class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
+ class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2"
>
<div class="text-xs uppercase tracking-wide">{{ $t("app.propagation") }}</div>
<div class="font-semibold text-gray-900 dark:text-white">
@@ -102,20 +102,30 @@
<div
class="sticky top-0 z-10 py-3 sm:py-4 mb-2 border-b border-gray-200/50 dark:border-zinc-800/50 bg-transparent min-w-0"
>
- <div class="relative w-full max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto min-w-0 px-0">
+ <div class="relative w-full max-w-6xl xl:max-w-7xl 2xl:max-w-360 mx-auto min-w-0 px-0">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<MaterialDesignIcon icon-name="magnify" class="size-5 text-gray-400" />
</div>
<input
- v-model="searchQuery"
- type="text"
- class="w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl py-3 pl-12 pr-4 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-none transition-all shadow-sm"
+ :value="searchQuery"
+ type="search"
+ inputmode="search"
+ enterkeyhint="search"
+ autocomplete="off"
+ autocorrect="off"
+ autocapitalize="none"
+ spellcheck="false"
+ class="w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl py-3 pl-12 pr-4 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-hidden transition-all shadow-xs"
:placeholder="$t('app.search_settings') || 'Search settings...'"
+ @input="onSettingsSearchInput"
+ @change="onSettingsSearchInput"
+ @compositionend="onSettingsSearchCompositionEnd"
/>
<button
- v-if="searchQuery"
+ v-if="settingsSearchActive"
+ type="button"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
- @click="searchQuery = ''"
+ @click="clearSettingsSearch"
>
<MaterialDesignIcon icon-name="close-circle" class="size-5" />
</button>
@@ -124,7 +134,7 @@
<!-- no results -->
<div
- v-if="searchQuery && !hasSearchResults"
+ v-if="settingsSearchActive && !hasSearchResults"
class="flex flex-col items-center justify-center py-12 text-center"
>
<div
@@ -133,10 +143,11 @@
<MaterialDesignIcon icon-name="magnify-close" class="size-8 text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">No results found</h3>
- <p class="text-gray-500 dark:text-gray-400">No settings match "{{ searchQuery }}"</p>
+ <p class="text-gray-500 dark:text-gray-400">No settings match "{{ settingsSearchDisplay }}"</p>
<button
+ type="button"
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition font-semibold text-sm"
- @click="searchQuery = ''"
+ @click="clearSettingsSearch"
>
Clear search
</button>
@@ -148,9 +159,7 @@
class="columns-1 md:columns-2 xl:columns-2 2xl:columns-3 gap-x-8 gap-y-0"
>
<SettingsSectionBlock
- v-show="
- matchesSearch('stranger', 'attachments', 'trust', 'block', 'banner', 'unknown', 'contact')
- "
+ v-show="matchesSearch(...sectionKeywords.strangerProtection)"
eyebrow="Security"
:title="$t('app.stranger_protection')"
:description="$t('app.stranger_protection_description')"
@@ -296,7 +305,7 @@
<label
class="flex items-center gap-2 text-sm text-gray-800 dark:text-gray-200 cursor-pointer"
>
- <input v-model="stickerImportReplaceDuplicates" type="checkbox" class="rounded" />
+ <input v-model="stickerImportReplaceDuplicates" type="checkbox" class="rounded-sm" />
{{ $t("stickers.replace_duplicates") }}
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -360,7 +369,7 @@
<label
class="flex items-center gap-2 text-sm text-gray-800 dark:text-gray-200 cursor-pointer"
>
- <input v-model="gifImportReplaceDuplicates" type="checkbox" class="rounded" />
+ <input v-model="gifImportReplaceDuplicates" type="checkbox" class="rounded-sm" />
{{ $t("gifs.replace_duplicates") }}
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -1120,7 +1129,7 @@
<input
id="detailed-outbound-send-status"
type="checkbox"
- class="mt-1 rounded border-gray-300 dark:border-zinc-600"
+ class="mt-1 rounded-sm border-gray-300 dark:border-zinc-600"
:checked="GlobalState.detailedOutboundSendStatus"
@change="onDetailedOutboundSendStatusChange"
/>
@@ -1134,6 +1143,26 @@
</label>
</div>
+ <div
+ class="flex items-start gap-3 rounded-xl border border-gray-200 dark:border-zinc-700 px-3 py-2.5"
+ >
+ <input
+ id="message-timestamp-grouping"
+ type="checkbox"
+ class="mt-1 rounded-sm border-gray-300 dark:border-zinc-600"
+ :checked="GlobalState.messageTimestampGroupingEnabled"
+ @change="onMessageTimestampGroupingChange"
+ />
+ <label for="message-timestamp-grouping" class="min-w-0 cursor-pointer">
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+ {{ $t("app.message_timestamp_grouping") }}
+ </div>
+ <div class="text-xs text-gray-500 dark:text-zinc-400 mt-0.5">
+ {{ $t("app.message_timestamp_grouping_description") }}
+ </div>
+ </label>
+ </div>
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
@@ -1289,21 +1318,23 @@
</div>
</section>
- <!-- Location -->
+ <!-- Location (map & coordinates) -->
<section
v-show="matchesSearch(...sectionKeywords.location)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
<div>
- <div class="settings-section__eyebrow">Privacy</div>
- <h2>Location</h2>
- <p>Manage how your location is shared.</p>
+ <div class="settings-section__eyebrow">{{ $t("app.settings_map_eyebrow") }}</div>
+ <h2>{{ $t("app.location") }}</h2>
+ <p>{{ $t("app.location_manage_desc") }}</p>
</div>
</header>
<div class="settings-section__body space-y-4">
<div class="space-y-2">
- <div class="text-sm font-medium text-gray-900 dark:text-gray-100">Location Source</div>
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+ {{ $t("app.location_source") }}
+ </div>
<select
v-model="config.location_source"
class="input-field"
@@ -1311,21 +1342,20 @@
updateConfig({ location_source: config.location_source }, 'location_source')
"
>
- <option value="browser">Automatic (Browser)</option>
- <option value="manual">Manual</option>
+ <option value="browser">{{ $t("app.location_source_browser") }}</option>
+ <option value="manual">{{ $t("app.location_source_manual") }}</option>
</select>
<div
v-if="config.location_source === 'browser'"
class="text-xs text-gray-600 dark:text-gray-400"
>
- Uses your browser's geolocation API. Note: In the desktop app, this can use Google
- services, which is blocked by CORS so you would need to specifically allow it.
+ {{ $t("app.location_source_browser_desc") }}
</div>
<div
v-if="config.location_source === 'manual'"
class="text-xs text-gray-600 dark:text-gray-400"
>
- Use manually entered coordinates for maximum privacy.
+ {{ $t("app.location_source_manual_desc") }}
</div>
</div>
@@ -1385,69 +1415,6 @@
/>
</div>
</div>
-
- <div class="pt-4 border-t border-gray-100 dark:border-zinc-800 space-y-4">
- <label class="setting-toggle">
- <Toggle
- id="telemetry-enabled"
- v-model="config.telemetry_enabled"
- @update:model-value="
- updateConfig(
- { telemetry_enabled: config.telemetry_enabled },
- 'telemetry_enabled'
- )
- "
- />
- <span class="setting-toggle__label">
- <span class="setting-toggle__title">{{ $t("app.telemetry_enabled") }}</span>
- <span class="setting-toggle__description">{{
- $t("app.telemetry_description")
- }}</span>
- </span>
- </label>
- </div>
-
- <div
- v-if="config.telemetry_enabled"
- class="pt-4 border-t border-gray-100 dark:border-zinc-800 space-y-4"
- >
- <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
- {{ $t("app.telemetry_trusted_peers") }}
- </div>
- <div v-if="trustedTelemetryPeers.length === 0" class="text-xs text-gray-500 italic">
- {{ $t("app.telemetry_no_trusted_peers") }}
- </div>
- <div v-else class="space-y-2">
- <div
- v-for="peer in trustedTelemetryPeers"
- :key="peer.id"
- class="flex items-center justify-between p-2 rounded-xl bg-gray-50 dark:bg-zinc-800 border border-gray-100 dark:border-zinc-700"
- >
- <div class="flex items-center gap-3">
- <div
- class="size-8 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-500 flex items-center justify-center"
- >
- <MaterialDesignIcon icon-name="account" class="size-5" />
- </div>
- <div class="min-w-0">
- <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
- {{ peer.name }}
- </div>
- <div class="text-[10px] text-gray-500 font-mono truncate">
- {{ peer.remote_identity_hash }}
- </div>
- </div>
- </div>
- <button
- class="p-2 text-gray-400 hover:text-red-500 transition-colors"
- :title="$t('app.telemetry_revoke_trust')"
- @click="revokeTelemetryTrust(peer)"
- >
- <MaterialDesignIcon icon-name="shield-off-outline" class="size-5" />
- </button>
- </div>
- </div>
- </div>
</div>
</section>
@@ -1490,7 +1457,13 @@
'Network Security',
'app.blackhole_integration_enabled',
'app.blackhole_integration_description',
- 'app.announce_limits'
+ 'app.announce_limits',
+ 'app.announce_store_heading',
+ 'app.announce_store_lxmf',
+ 'app.announce_store_lxst',
+ 'app.announce_store_nomad',
+ 'app.announce_store_prop',
+ 'app.announce_store_git'
)
"
class="settings-section break-inside-avoid"
@@ -1531,6 +1504,79 @@
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("app.announce_limits_description") }}
</div>
+ <div class="text-xs font-medium text-gray-800 dark:text-gray-200">
+ {{ $t("app.announce_store_heading") }}
+ </div>
+ <div class="text-xs text-gray-600 dark:text-gray-400">
+ {{ $t("app.announce_store_description") }}
+ </div>
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
+ <label class="setting-toggle">
+ <Toggle
+ :model-value="config.announce_store_lxmf_delivery"
+ @update:model-value="
+ (v) => onAnnounceStoreToggle('announce_store_lxmf_delivery', v)
+ "
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("app.announce_store_lxmf")
+ }}</span>
+ </span>
+ </label>
+ <label class="setting-toggle">
+ <Toggle
+ :model-value="config.announce_store_lxst_telephony"
+ @update:model-value="
+ (v) => onAnnounceStoreToggle('announce_store_lxst_telephony', v)
+ "
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("app.announce_store_lxst")
+ }}</span>
+ </span>
+ </label>
+ <label class="setting-toggle">
+ <Toggle
+ :model-value="config.announce_store_nomadnetwork_node"
+ @update:model-value="
+ (v) => onAnnounceStoreToggle('announce_store_nomadnetwork_node', v)
+ "
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("app.announce_store_nomad")
+ }}</span>
+ </span>
+ </label>
+ <label class="setting-toggle">
+ <Toggle
+ :model-value="config.announce_store_lxmf_propagation"
+ @update:model-value="
+ (v) => onAnnounceStoreToggle('announce_store_lxmf_propagation', v)
+ "
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("app.announce_store_prop")
+ }}</span>
+ </span>
+ </label>
+ <label class="setting-toggle sm:col-span-2">
+ <Toggle
+ :model-value="config.announce_store_git_repositories"
+ @update:model-value="
+ (v) => onAnnounceStoreToggle('announce_store_git_repositories', v)
+ "
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("app.announce_store_git")
+ }}</span>
+ </span>
+ </label>
+ </div>
<div
class="text-xs font-semibold text-gray-700 dark:text-zinc-300 uppercase tracking-wide"
>
@@ -1705,7 +1751,7 @@
<!-- Blocked -->
<section
- v-show="matchesSearch('Privacy', 'Banished', 'Manage Banished users and nodes')"
+ v-show="matchesSearch(...sectionKeywords.blocked)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
@@ -1724,6 +1770,138 @@
</div>
</section>
+ <SettingsSectionBlock
+ v-show="matchesSearch(...sectionKeywords.privacyData)"
+ :eyebrow="$t('app.privacy_eyebrow')"
+ :title="$t('app.privacy_data_title')"
+ :description="$t('app.privacy_data_description')"
+ body-class="space-y-4"
+ >
+ <div class="space-y-3">
+ <div
+ class="text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-zinc-400"
+ >
+ {{ $t("app.privacy_subsection_device") }}
+ </div>
+ <label class="setting-toggle">
+ <Toggle
+ id="local-message-auto-delete"
+ v-model="config.local_message_auto_delete_enabled"
+ @update:model-value="onLocalMessageAutoDeleteEnabledChange"
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{
+ $t("app.local_message_auto_delete_title")
+ }}</span>
+ <span class="setting-toggle__description">{{
+ $t("app.local_message_auto_delete_description")
+ }}</span>
+ </span>
+ </label>
+ <div
+ v-if="config.local_message_auto_delete_enabled"
+ class="grid grid-cols-1 sm:grid-cols-2 gap-3 pl-0 sm:pl-1"
+ >
+ <div class="space-y-1">
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+ {{ $t("app.local_message_auto_delete_age") }}
+ </div>
+ <div class="flex flex-wrap items-center gap-2">
+ <input
+ v-model.number="config.local_message_auto_delete_value"
+ type="number"
+ min="1"
+ :max="config.local_message_auto_delete_unit === 'months' ? 120 : 10000"
+ class="input-field w-24"
+ :aria-label="$t('app.local_message_auto_delete_age')"
+ @input="onLocalMessageAutoDeleteParamsChange"
+ />
+ <select
+ v-model="config.local_message_auto_delete_unit"
+ class="input-field min-w-[7rem]"
+ :aria-label="$t('app.local_message_auto_delete_unit_aria')"
+ @change="onLocalMessageAutoDeleteParamsChange"
+ >
+ <option value="days">
+ {{ $t("app.local_message_auto_delete_unit_days") }}
+ </option>
+ <option value="months">
+ {{ $t("app.local_message_auto_delete_unit_months") }}
+ </option>
+ </select>
+ </div>
+ <div class="text-xs text-gray-600 dark:text-gray-400">
+ {{ $t("app.local_message_auto_delete_month_note") }}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="border-t border-gray-200 dark:border-zinc-800 pt-4 space-y-4">
+ <div
+ class="text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-zinc-400"
+ >
+ {{ $t("app.privacy_subsection_telemetry") }}
+ </div>
+ <label class="setting-toggle">
+ <Toggle
+ id="telemetry-enabled"
+ v-model="config.telemetry_enabled"
+ @update:model-value="
+ updateConfig(
+ { telemetry_enabled: config.telemetry_enabled },
+ 'telemetry_enabled'
+ )
+ "
+ />
+ <span class="setting-toggle__label">
+ <span class="setting-toggle__title">{{ $t("app.telemetry_enabled") }}</span>
+ <span class="setting-toggle__description">{{
+ $t("app.telemetry_description")
+ }}</span>
+ </span>
+ </label>
+ <div v-if="config.telemetry_enabled" class="space-y-4">
+ <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
+ {{ $t("app.telemetry_trusted_peers") }}
+ </div>
+ <div v-if="trustedTelemetryPeers.length === 0" class="text-xs text-gray-500 italic">
+ {{ $t("app.telemetry_no_trusted_peers") }}
+ </div>
+ <div v-else class="space-y-2">
+ <div
+ v-for="peer in trustedTelemetryPeers"
+ :key="peer.id"
+ class="flex items-center justify-between p-2 rounded-xl bg-gray-50 dark:bg-zinc-800 border border-gray-100 dark:border-zinc-700"
+ >
+ <div class="flex items-center gap-3">
+ <div
+ class="size-8 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-500 flex items-center justify-center"
+ >
+ <MaterialDesignIcon icon-name="account" class="size-5" />
+ </div>
+ <div class="min-w-0">
+ <div class="text-sm font-bold text-gray-900 dark:text-white truncate">
+ {{ peer.name }}
+ </div>
+ <div class="text-[10px] text-gray-500 font-mono truncate">
+ {{ peer.remote_identity_hash }}
+ </div>
+ </div>
+ </div>
+ <button
+ class="p-2 text-gray-400 hover:text-red-500 transition-colors"
+ :title="$t('app.telemetry_revoke_trust')"
+ @click="revokeTelemetryTrust(peer)"
+ >
+ <MaterialDesignIcon icon-name="shield-off-outline" class="size-5" />
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </SettingsSectionBlock>
+
<!-- Authentication -->
<section
v-show="matchesSearch(...sectionKeywords.auth)"
@@ -1759,51 +1937,6 @@
</div>
</section>
- <!-- Translator -->
- <section
- v-show="matchesSearch(...sectionKeywords.translator)"
- class="settings-section break-inside-avoid"
- >
- <header class="settings-section__header">
- <div>
- <div class="settings-section__eyebrow">i18n</div>
- <h2>{{ $t("app.translator") }}</h2>
- <p>{{ $t("translator.description") }}</p>
- </div>
- </header>
- <div class="settings-section__body space-y-4">
- <label class="setting-toggle">
- <Toggle
- id="translator-enabled"
- v-model="config.translator_enabled"
- @update:model-value="onTranslatorEnabledChange"
- />
- <span class="setting-toggle__label">
- <span class="setting-toggle__title">{{ $t("app.translator_enabled") }}</span>
- <span class="setting-toggle__description">{{
- $t("app.translator_description")
- }}</span>
- </span>
- </label>
-
- <div v-if="config.translator_enabled" class="space-y-2">
- <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
- {{ $t("app.libretranslate_url") }}
- </div>
- <input
- v-model="config.libretranslate_url"
- type="text"
- placeholder="http://localhost:5000"
- class="input-field"
- @input="onTranslatorConfigChange"
- />
- <div class="text-xs text-gray-600 dark:text-gray-400">
- {{ $t("app.libretranslate_url_description") }}
- </div>
- </div>
- </div>
- </section>
-
<!-- Sources & Infrastructure -->
<section
v-show="matchesSearch(...sectionKeywords.infrastructure)"
@@ -1925,14 +2058,14 @@
</div>
</section>
- <!-- Messages -->
+ <!-- Messages (LXMF delivery, retries, inbound stamps) -->
<section
v-show="matchesSearch(...sectionKeywords.messages)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
<div>
- <div class="settings-section__eyebrow">{{ $t("app.reliability") }}</div>
+ <div class="settings-section__eyebrow">{{ $t("app.lxmf_settings_eyebrow") }}</div>
<h2>{{ $t("app.messages") }}</h2>
<p>{{ $t("app.messages_description") }}</p>
</div>
@@ -2150,12 +2283,12 @@
type="number"
min="0.001"
step="any"
- class="input-field max-w-[10rem]"
+ class="input-field max-w-40"
@input="onLxmfIncomingDeliveryCustomChange"
/>
<select
v-model="lxmfIncomingDeliveryCustomUnit"
- class="input-field max-w-[8rem]"
+ class="input-field max-w-32"
@change="onLxmfIncomingDeliveryCustomChange"
>
<option value="mb">{{ $t("app.incoming_message_size_unit_mb") }}</option>
@@ -2337,6 +2470,8 @@ import {
incomingDeliveryBytesFromPresetKey,
syncIncomingDeliveryFieldsFromBytes,
} from "../../js/settings/incomingDeliveryLimit";
+import { normalizeRetentionValue } from "../../js/localMessageRetention";
+import { matchesSettingSearch, normalizeSearchString } from "../../js/settingsSearchUtils";
export default {
name: "SettingsPage",
@@ -2379,6 +2514,11 @@ export default {
banished_text: "BANISHED",
banished_color: "#dc2626",
blackhole_integration_enabled: true,
+ announce_store_lxmf_delivery: true,
+ announce_store_lxst_telephony: true,
+ announce_store_nomadnetwork_node: true,
+ announce_store_lxmf_propagation: true,
+ announce_store_git_repositories: true,
announce_max_stored_lxmf_delivery: 1000,
announce_max_stored_nomadnetwork_node: 1000,
announce_max_stored_lxmf_propagation: 1000,
@@ -2413,6 +2553,9 @@ export default {
nomad_render_html_enabled: true,
nomad_render_plaintext_enabled: true,
nomad_default_page_path: "/page/index.mu",
+ local_message_auto_delete_enabled: false,
+ local_message_auto_delete_value: 30,
+ local_message_auto_delete_unit: "days",
},
saveTimeouts: {},
lxmfIncomingDeliveryPreset: "10mb",
@@ -2433,6 +2576,27 @@ export default {
visualiserShowDisabledInterfaces: false,
visualiserShowDiscoveredInterfaces: false,
sectionKeywords: {
+ strangerProtection: [
+ "Security",
+ "app.stranger_protection",
+ "app.stranger_protection_description",
+ "app.block_stranger_attachments",
+ "app.block_stranger_attachments_description",
+ "app.block_all_from_strangers",
+ "app.block_all_from_strangers_description",
+ "app.show_unknown_contact_banner",
+ "app.show_unknown_contact_banner_description",
+ "app.warn_on_stranger_links",
+ "app.warn_on_stranger_links_description",
+ "stranger",
+ "attachments",
+ "trust",
+ "block",
+ "banner",
+ "unknown",
+ "contact",
+ "links",
+ ],
visualiser: [
"Visualiser",
"Network Visualiser",
@@ -2578,6 +2742,12 @@ export default {
"app.blackhole_integration_enabled",
"app.blackhole_integration_description",
"app.announce_limits",
+ "app.announce_store_heading",
+ "app.announce_store_lxmf",
+ "app.announce_store_lxst",
+ "app.announce_store_nomad",
+ "app.announce_store_prop",
+ "app.announce_store_git",
"app.announce_limit_lxmf",
"app.announce_limit_nomadnet",
"app.announce_limit_prop",
@@ -2601,18 +2771,9 @@ export default {
],
blocked: ["Privacy", "Banished", "Manage Banished users and nodes"],
auth: ["Security", "Authentication", "password", "Protect your instance with a password"],
- translator: [
- "i18n",
- "app.translator",
- "translator.description",
- "app.translator_enabled",
- "app.translator_description",
- "app.libretranslate_url",
- "app.libretranslate_url_description",
- ],
infrastructure: ["Infrastructure", "Sources & Mirroring", "gitea", "documentation", "download", "urls"],
messages: [
- "reliability",
+ "app.lxmf_settings_eyebrow",
"app.messages",
"app.messages_description",
"app.auto_resend_title",
@@ -2645,24 +2806,46 @@ export default {
"app.propagation_stamp_description",
],
location: [
+ "app.location",
+ "app.location_manage_desc",
+ "app.location_source",
+ "Map",
"Location",
"GPS",
- "Privacy",
"manual",
"latitude",
"longitude",
"altitude",
- "telemetry",
- "trusted peers",
+ ],
+ privacyData: [
+ "app.privacy_data_title",
+ "app.privacy_data_description",
+ "app.local_message_auto_delete_title",
+ "app.local_message_auto_delete_description",
+ "app.local_message_auto_delete_age",
+ "app.telemetry_enabled",
+ "app.telemetry_description",
+ "app.telemetry_trusted_peers",
+ "ephemeral",
+ "retention",
+ "Privacy",
],
shortcuts: ["Keyboard Shortcuts", "actions", "workflow"],
},
};
},
computed: {
+ settingsSearchActive() {
+ return normalizeSearchString(this.searchQuery).length > 0;
+ },
+ settingsSearchDisplay() {
+ return normalizeSearchString(this.searchQuery) || this.searchQuery;
+ },
hasSearchResults() {
- if (!this.searchQuery) return true;
- return Object.values(this.sectionKeywords).some((keywords) => this.matchesSearch(...keywords));
+ if (!normalizeSearchString(this.searchQuery)) return true;
+ return Object.values(this.sectionKeywords).some((keywords) =>
+ matchesSettingSearch(keywords, (k) => this.$t(k), this.searchQuery)
+ );
},
safeConfig() {
if (!this.config) {
@@ -2747,15 +2930,21 @@ export default {
console.error(e);
}
},
+ onSettingsSearchInput(e) {
+ const el = e?.target;
+ if (!el || el.tagName !== "INPUT") return;
+ this.searchQuery = el.value;
+ },
+ onSettingsSearchCompositionEnd(e) {
+ const el = e?.target;
+ if (!el || el.tagName !== "INPUT") return;
+ this.searchQuery = el.value;
+ },
+ clearSettingsSearch() {
+ this.searchQuery = "";
+ },
matchesSearch(...texts) {
- if (!this.searchQuery) return true;
- const query = this.searchQuery.toLowerCase();
- return texts.some((text) => {
- if (!text) return false;
- // If it looks like a translation key, translate it
- const content = text.includes(".") ? this.$t(text) : text;
- return content.toLowerCase().includes(query);
- });
+ return matchesSettingSearch(texts, (k) => this.$t(k), this.searchQuery);
},
async onWebsocketMessage(message) {
const json = JSON.parse(message.data);
@@ -2896,6 +3085,10 @@ export default {
"announce_limits"
);
},
+ async onAnnounceStoreToggle(key, value) {
+ this.config[key] = value;
+ await this.updateConfig({ [key]: value }, key);
+ },
async copyValue(value, label) {
if (!value) {
ToastUtils.warning(`Nothing to copy for ${label}`);
@@ -3030,6 +3223,15 @@ export default {
// ignore
}
},
+ onMessageTimestampGroupingChange(event) {
+ const checked = event.target.checked;
+ GlobalState.messageTimestampGroupingEnabled = checked;
+ try {
+ localStorage.setItem("meshchatx_message_timestamp_grouping_enabled", checked ? "true" : "false");
+ } catch {
+ // ignore
+ }
+ },
async onLanguageChange() {
await this.updateConfig(
{
@@ -3312,6 +3514,30 @@ export default {
this.config.warn_on_stranger_links = value;
await this.updateConfig({ warn_on_stranger_links: value }, "stranger_protection");
},
+ async onLocalMessageAutoDeleteEnabledChange(value) {
+ this.config.local_message_auto_delete_enabled = value;
+ await this.updateConfig({ local_message_auto_delete_enabled: value }, "privacy_data");
+ },
+ onLocalMessageAutoDeleteParamsChange() {
+ if (this.saveTimeouts.localMessageAutoDelete) {
+ clearTimeout(this.saveTimeouts.localMessageAutoDelete);
+ }
+ this.saveTimeouts.localMessageAutoDelete = setTimeout(async () => {
+ const { value: v, unit: u } = normalizeRetentionValue(
+ this.config.local_message_auto_delete_value,
+ this.config.local_message_auto_delete_unit
+ );
+ this.config.local_message_auto_delete_value = v;
+ this.config.local_message_auto_delete_unit = u;
+ await this.updateConfig(
+ {
+ local_message_auto_delete_value: v,
+ local_message_auto_delete_unit: u,
+ },
+ "privacy_data"
+ );
+ }, 400);
+ },
async onBanishedEffectEnabledChange(value) {
this.config.banished_effect_enabled = value;
await this.updateConfig(
@@ -3386,26 +3612,6 @@ export default {
this.$router.push({ name: "auth" });
}
},
- async onTranslatorEnabledChange(value) {
- this.config.translator_enabled = value;
- await this.updateConfig(
- {
- translator_enabled: value,
- },
- "translator"
- );
- },
- async onTranslatorConfigChange() {
- if (this.saveTimeouts.translator) clearTimeout(this.saveTimeouts.translator);
- this.saveTimeouts.translator = setTimeout(async () => {
- await this.updateConfig(
- {
- libretranslate_url: this.config.libretranslate_url,
- },
- "translator"
- );
- }, 1000);
- },
async onGiteaConfigChange() {
if (this.saveTimeouts.gitea) clearTimeout(this.saveTimeouts.gitea);
this.saveTimeouts.gitea = setTimeout(async () => {
@@ -3914,6 +4120,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.settings-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8 flex flex-col break-inside-avoid;
}
@@ -3969,13 +4176,13 @@ export default {
font-family: "Roboto Mono", monospace;
}
.address-card {
- @apply relative border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02] space-y-2;
+ @apply relative border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2 space-y-2;
}
.address-card__label {
@apply text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400;
}
.address-card__value {
- @apply text-sm text-gray-900 dark:text-white break-words pr-16;
+ @apply text-sm text-gray-900 dark:text-white wrap-break-word pr-16;
}
.address-card__action {
@apply absolute top-3 right-3 inline-flex items-center gap-1 rounded-full border border-gray-200 dark:border-zinc-700 px-3 py-1 text-xs font-semibold text-gray-700 dark:text-gray-100 bg-white/70 dark:bg-zinc-900/60 hover:border-blue-400 dark:hover:border-blue-500 transition;
diff --git a/meshchatx/src/frontend/components/settings/ShortcutRecorder.vue b/meshchatx/src/frontend/components/settings/ShortcutRecorder.vue
index c3669967..0e86be7a 100644
--- a/meshchatx/src/frontend/components/settings/ShortcutRecorder.vue
+++ b/meshchatx/src/frontend/components/settings/ShortcutRecorder.vue
@@ -10,7 +10,7 @@
<kbd
v-for="key in keys"
:key="key"
- class="px-2 py-1 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs font-bold text-gray-600 dark:text-zinc-300 shadow-sm uppercase"
+ class="px-2 py-1 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs font-bold text-gray-600 dark:text-zinc-300 shadow-xs uppercase"
>
{{ formatKey(key) }}
</kbd>
@@ -22,7 +22,7 @@
<button
type="button"
- class="px-4 py-2 rounded-xl font-bold transition-all shadow-sm flex items-center gap-2"
+ class="px-4 py-2 rounded-xl font-bold transition-all shadow-xs flex items-center gap-2"
:class="[
isRecording
? 'bg-blue-600 text-white hover:bg-blue-700'
diff --git a/meshchatx/src/frontend/components/stickers/StickerPacksManager.vue b/meshchatx/src/frontend/components/stickers/StickerPacksManager.vue
index 990bfd88..184aefba 100644
--- a/meshchatx/src/frontend/components/stickers/StickerPacksManager.vue
+++ b/meshchatx/src/frontend/components/stickers/StickerPacksManager.vue
@@ -77,7 +77,7 @@
:src="stickerImageUrl(s.id)"
:image-type="s.image_type"
size="xs"
- class="rounded border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900"
+ class="rounded-sm border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900"
/>
</div>
<div v-else class="text-xs text-gray-500 dark:text-zinc-400 italic mt-1">
@@ -88,7 +88,7 @@
<div
v-if="createOpen"
- class="fixed inset-0 z-[150] flex items-center justify-center bg-black/60 p-4"
+ class="fixed inset-0 z-150 flex items-center justify-center bg-black/60 p-4"
@click.self="createOpen = false"
>
<div
diff --git a/meshchatx/src/frontend/components/tools/BotsPage.vue b/meshchatx/src/frontend/components/tools/BotsPage.vue
index 2e33e409..7c467af5 100644
--- a/meshchatx/src/frontend/components/tools/BotsPage.vue
+++ b/meshchatx/src/frontend/components/tools/BotsPage.vue
@@ -6,14 +6,25 @@
class="flex-1 overflow-y-auto w-full px-3 sm:px-4 md:px-5 lg:px-8 py-4 sm:py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]"
>
<div class="space-y-8 w-full max-w-4xl mx-auto">
- <div class="space-y-2 border-b border-gray-200 dark:border-zinc-800 pb-6">
- <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
- {{ $t("bots.bot_framework") }}
- </div>
- <div class="text-2xl font-semibold text-gray-900 dark:text-white">{{ $t("bots.title") }}</div>
- <div class="text-sm text-gray-600 dark:text-gray-300">
- {{ $t("bots.description") }}
+ <div
+ class="flex flex-wrap items-start justify-between gap-3 border-b border-gray-200 dark:border-zinc-800 pb-6"
+ >
+ <div class="space-y-2 min-w-0">
+ <div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
+ {{ $t("bots.bot_framework") }}
+ </div>
+ <div class="text-2xl font-semibold text-gray-900 dark:text-white">{{ $t("bots.title") }}</div>
+ <div class="text-sm text-gray-600 dark:text-gray-300">
+ {{ $t("bots.description") }}
+ </div>
</div>
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-300 hover:underline shrink-0"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
</div>
<div class="space-y-6">
@@ -159,7 +170,7 @@
<input
v-model="editingNameDraft"
type="text"
- class="input-field text-xs py-1 h-8 px-2 min-w-0 flex-1 max-w-[10rem] sm:max-w-[12rem]"
+ class="input-field text-xs py-1 h-8 px-2 min-w-0 flex-1 max-w-40 sm:max-w-48"
maxlength="256"
@keydown.enter.prevent="saveBotName(bot)"
@keydown.escape="cancelEditName"
@@ -246,7 +257,7 @@
<div
v-if="selectedTemplate"
- class="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/50"
+ class="fixed inset-0 z-100 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/50"
@click.self="selectedTemplate = null"
>
<div
@@ -539,6 +550,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.glass-label {
@apply block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1;
}
diff --git a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
index ecdcfb57..1d416ded 100644
--- a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
+++ b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
@@ -5,7 +5,9 @@
<div class="flex-1 overflow-y-auto w-full pb-[max(1rem,env(safe-area-inset-bottom))]">
<div class="p-3 sm:p-4 md:p-6 max-w-5xl mx-auto w-full space-y-4 min-w-0">
<!-- header -->
- <div class="border-b border-gray-200 dark:border-zinc-800 pb-4">
+ <div
+ class="flex flex-wrap items-start justify-between gap-3 border-b border-gray-200 dark:border-zinc-800 pb-4"
+ >
<div class="flex items-start gap-3 min-w-0">
<div
class="p-2 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-lg shrink-0"
@@ -21,6 +23,13 @@
</p>
</div>
</div>
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-300 hover:underline shrink-0"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
@@ -199,7 +208,7 @@
</div>
<button
type="button"
- class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-sm"
+ class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-xs"
title="Copy URI"
@click="copyUri"
>
@@ -258,7 +267,7 @@
<div
v-if="isIngestScannerModalOpen"
- class="fixed inset-0 z-[210] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
+ class="fixed inset-0 z-210 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs"
@click.self="closeIngestScannerModal"
>
<div class="w-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -598,6 +607,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-50 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition;
}
diff --git a/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue b/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue
index e289bc82..c695cf73 100644
--- a/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue
+++ b/meshchatx/src/frontend/components/tools/RNGitExplorerPage.vue
@@ -2,7 +2,7 @@
<template>
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
- <div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-sm z-10">
+ <div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-xs z-10">
<div class="px-4 py-3 md:px-6 md:py-4 flex flex-wrap items-center justify-between gap-3 min-w-0">
<div class="flex items-center gap-3 min-w-0">
<div class="p-2 bg-teal-100 dark:bg-teal-900/30 rounded-xl shrink-0">
@@ -25,7 +25,7 @@
class="inline-flex items-center gap-2 text-sm text-teal-600 dark:text-teal-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
- {{ $t("rngit_explorer.back_tools") }}
+ {{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
</div>
@@ -123,7 +123,7 @@
<input
v-model="forPush"
type="checkbox"
- class="rounded border-gray-300 shrink-0 mt-0.5"
+ class="rounded-sm border-gray-300 shrink-0 mt-0.5"
:disabled="busy"
/>
<span>{{ $t("rngit_explorer.for_push") }}</span>
@@ -204,7 +204,7 @@
</div>
<div class="flex flex-wrap gap-2 items-start">
<pre
- class="flex-1 min-w-0 p-2 rounded bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 overflow-x-auto whitespace-pre-wrap break-all"
+ class="flex-1 min-w-0 p-2 rounded-sm bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 overflow-x-auto whitespace-pre-wrap break-all"
>{{ selected.clone_command }}</pre
>
<button
@@ -227,7 +227,7 @@
}}</span>
</div>
<pre
- class="p-2 rounded bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
+ class="p-2 rounded-sm bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
>{{ selected.refs_preview }}</pre
>
</div>
diff --git a/meshchatx/src/frontend/components/tools/RNPathPage.vue b/meshchatx/src/frontend/components/tools/RNPathPage.vue
index 6b192427..f9258f5e 100644
--- a/meshchatx/src/frontend/components/tools/RNPathPage.vue
+++ b/meshchatx/src/frontend/components/tools/RNPathPage.vue
@@ -4,7 +4,7 @@
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
<!-- header -->
<div
- class="flex flex-wrap items-center gap-2 px-3 sm:px-4 md:px-6 py-3 sm:py-4 bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800"
+ class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 md:px-6 py-3 sm:py-4 bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800"
>
<div class="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
<div class="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg shrink-0">
@@ -21,7 +21,14 @@
</div>
</div>
- <div class="ml-auto flex items-center gap-2 shrink-0">
+ <div class="flex items-center gap-2 shrink-0 w-full sm:w-auto justify-end">
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-indigo-600 dark:text-indigo-300 hover:underline shrink-0"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
<button
class="p-2 text-gray-500 hover:text-indigo-500 dark:text-gray-400 dark:hover:text-indigo-400 transition-colors"
title="Refresh"
@@ -143,12 +150,12 @@
{{ path.hash }}
</span>
<span
- class="px-2 py-0.5 text-[10px] font-bold bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded uppercase tracking-wider"
+ class="px-2 py-0.5 text-[10px] font-bold bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded-sm uppercase tracking-wider"
>
{{ path.hops }} {{ path.hops === 1 ? "hop" : "hops" }}
</span>
<span
- class="px-2 py-0.5 text-[10px] font-bold rounded uppercase tracking-wider"
+ class="px-2 py-0.5 text-[10px] font-bold rounded-sm uppercase tracking-wider"
:class="getStateColor(path.state)"
>
{{ getStateText(path.state) }}
@@ -236,7 +243,7 @@
</span>
<span
v-if="rate.blocked_until > Date.now() / 1000"
- class="px-2 py-0.5 text-[10px] font-bold bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded"
+ class="px-2 py-0.5 text-[10px] font-bold bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-sm"
>
RATE LIMITED
</span>
@@ -538,6 +545,7 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-xl focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 dark:focus:ring-indigo-500 dark:focus:border-indigo-500 block w-full p-3 text-gray-900 dark:text-gray-100 transition;
}
diff --git a/meshchatx/src/frontend/components/tools/RNPathTracePage.vue b/meshchatx/src/frontend/components/tools/RNPathTracePage.vue
index 5a6f2e05..dfedbdd1 100644
--- a/meshchatx/src/frontend/components/tools/RNPathTracePage.vue
+++ b/meshchatx/src/frontend/components/tools/RNPathTracePage.vue
@@ -3,9 +3,9 @@
<template>
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
<!-- header -->
- <div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-sm z-10">
+ <div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-xs z-10">
<div class="px-4 py-3 md:px-6 md:py-4 flex flex-wrap items-center justify-between gap-3 md:gap-4 min-w-0">
- <div class="flex items-center gap-3">
+ <div class="flex items-center gap-3 min-w-0">
<div class="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl shrink-0">
<MaterialDesignIcon
icon-name="map-marker-path"
@@ -22,7 +22,14 @@
</div>
</div>
- <div class="flex items-center gap-2">
+ <div class="flex items-center gap-2 shrink-0">
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-indigo-600 dark:text-indigo-300 hover:underline"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
<button
v-if="traceResult"
class="p-2 text-gray-400 hover:text-indigo-500 dark:hover:text-indigo-400 transition-colors shrink-0"
@@ -49,7 +56,7 @@
v-model="destinationHash"
type="text"
placeholder="input destination hash"
- class="w-full pl-4 pr-12 py-3 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-sm md:text-base font-mono focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all dark:text-white"
+ class="w-full pl-4 pr-12 py-3 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-sm md:text-base font-mono focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-hidden transition-all dark:text-white"
@keyup.enter="runTrace"
/>
<div
@@ -60,7 +67,7 @@
</div>
<button
type="button"
- class="w-full sm:w-auto sm:min-w-[3rem] h-12 sm:h-14 px-4 sm:px-0 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg flex items-center justify-center gap-2 transition active:scale-95 disabled:opacity-50 shrink-0"
+ class="w-full sm:w-auto sm:min-w-12 h-12 sm:h-14 px-4 sm:px-0 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg flex items-center justify-center gap-2 transition active:scale-95 disabled:opacity-50 shrink-0"
:disabled="!isValidHash || isLoading"
title="Trace Path"
@click="runTrace"
@@ -211,7 +218,7 @@
v-if="
traceResult.path[idx + 1].type !== 'unknown' && node.type !== 'unknown'
"
- class="absolute right-0 -top-1 w-2 h-2 rounded-full bg-indigo-500 shadow-sm shadow-indigo-500/50"
+ class="absolute right-0 -top-1 w-2 h-2 rounded-full bg-indigo-500 shadow-xs shadow-indigo-500/50"
></div>
</div>
</template>
@@ -259,7 +266,7 @@
</div>
<div
v-if="node.interface"
- class="inline-flex items-center gap-1 mt-1.5 px-2 py-0.5 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 rounded text-[9px] font-bold uppercase tracking-wider"
+ class="inline-flex items-center gap-1 mt-1.5 px-2 py-0.5 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 rounded-sm text-[9px] font-bold uppercase tracking-wider"
>
<MaterialDesignIcon icon-name="router-wireless" class="size-3" />
{{ node.interface }}
diff --git a/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue b/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue
index 58d5b1f1..6c5d2b06 100644
--- a/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue
+++ b/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue
@@ -21,6 +21,13 @@
</div>
<div class="ml-auto flex flex-wrap items-center justify-end gap-1 sm:gap-2 shrink-0">
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-violet-600 dark:text-violet-300 hover:underline"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
<button
class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors flex items-center gap-2 text-sm font-medium"
@click="showAdvanced = !showAdvanced"
@@ -39,12 +46,6 @@
<MaterialDesignIcon icon-name="open-in-new" class="size-5" />
<span class="hidden sm:inline">{{ $t("tools.rnode_flasher.original") }}</span>
</a>
- <button
- class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
- @click="$router.push({ name: 'tools' })"
- >
- <MaterialDesignIcon icon-name="close" class="size-5" />
- </button>
</div>
</div>
@@ -150,15 +151,15 @@
<div class="flex items-center gap-4">
<a
target="_blank"
- :href="`${giteaBaseUrl}/Reticulum/rnode-flasher`"
+ href="https://github.com/liamcottle/rnode-flasher"
class="text-blue-500 hover:underline text-sm font-bold"
- >RNode Flasher GH</a
+ >RNode Flasher</a
>
<a
target="_blank"
- :href="`${giteaBaseUrl}/Reticulum/RNode_Firmware`"
+ href="https://github.com/markqvist/RNode_Firmware"
class="text-blue-500 hover:underline text-sm font-bold"
- >RNode Firmware GH</a
+ >RNode Firmware</a
>
</div>
</div>
@@ -198,7 +199,6 @@ import WifiTransport from "../../js/rnode/transports/WifiTransport.js";
import { diagnose } from "../../js/rnode/Diagnostics.js";
import ToastUtils from "../../js/ToastUtils.js";
-import GlobalState from "../../js/GlobalState";
export default {
name: "RNodeFlasherPage",
@@ -249,9 +249,6 @@ export default {
recommendedFirmwareFilename() {
return this.selectedModel?.firmware_filename ?? this.selectedProduct?.firmware_filename;
},
- giteaBaseUrl() {
- return GlobalState.config?.gitea_base_url || "https://git.quad4.io";
- },
canFlash() {
if (this.connectionMethod === TRANSPORT_WIFI) {
return Boolean(this.wifiHost && this.firmwareFile);
@@ -299,7 +296,7 @@ export default {
},
async fetchLatestRelease() {
try {
- const response = await fetch("/api/v1/tools/rnode/latest_release?repo=Reticulum/RNode_Firmware");
+ const response = await fetch("/api/v1/tools/rnode/latest_release");
if (response.ok) {
this.latestRelease = await response.json();
}
@@ -314,8 +311,7 @@ export default {
if (asset?.browser_download_url) {
return asset.browser_download_url;
}
- const base = (this.giteaBaseUrl || "https://git.quad4.io").replace(/\/$/, "");
- return `${base}/Reticulum/RNode_Firmware/releases/latest/download/${filename}`;
+ return `https://github.com/markqvist/RNode_Firmware/releases/latest/download/${filename}`;
},
async downloadRecommendedFirmware() {
const assetUrl = this._resolveRecommendedAssetUrl();
diff --git a/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue b/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue
index 4749c7a5..6c0ad2c0 100644
--- a/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue
+++ b/meshchatx/src/frontend/components/tools/RepositoryServerPage.vue
@@ -40,7 +40,7 @@
class="inline-flex items-center gap-2 text-sm text-sky-600 dark:text-sky-300 hover:underline"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
- {{ $t("tools.repository_server.back_tools") }}
+ {{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
</div>
@@ -67,7 +67,7 @@
{{ $t("tools.repository_server.http_heading") }}
</h2>
<div class="flex flex-wrap gap-3 items-end">
- <label class="flex flex-col gap-1 text-xs min-w-[10rem]">
+ <label class="flex flex-col gap-1 text-xs min-w-40">
<span class="text-gray-500 dark:text-zinc-500">{{
$t("tools.repository_server.host_label")
}}</span>
diff --git a/meshchatx/src/frontend/components/tools/ReticulumConfigEditorPage.vue b/meshchatx/src/frontend/components/tools/ReticulumConfigEditorPage.vue
index c15872e5..c123699a 100644
--- a/meshchatx/src/frontend/components/tools/ReticulumConfigEditorPage.vue
+++ b/meshchatx/src/frontend/components/tools/ReticulumConfigEditorPage.vue
@@ -3,7 +3,7 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
<div
- class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-sm shrink-0 min-w-0"
+ class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-xs shrink-0 min-w-0"
>
<div class="flex items-center gap-2 sm:gap-3 min-w-0">
<div class="bg-blue-100 dark:bg-blue-900/30 p-1.5 rounded-xl shrink-0">
@@ -25,13 +25,20 @@
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
- <button type="button" class="secondary-chip !py-1 !px-3" :disabled="loading" @click="loadConfig">
+ <RouterLink
+ to="/tools"
+ class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-300 hover:underline"
+ >
+ <MaterialDesignIcon icon-name="arrow-left" class="size-4" />
+ {{ $t("tools.back_to_tools") }}
+ </RouterLink>
+ <button type="button" class="secondary-chip py-1! px-3!" :disabled="loading" @click="loadConfig">
<MaterialDesignIcon icon-name="refresh" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">{{ $t("tools.reticulum_config_editor.reload") }}</span>
</button>
<button
type="button"
- class="secondary-chip !py-1 !px-3 !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
+ class="secondary-chip py-1! px-3! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
:disabled="loading || resetting"
@click="restoreDefaults"
>
@@ -40,7 +47,7 @@
</button>
<button
type="button"
- class="secondary-chip !py-1 !px-3"
+ class="secondary-chip py-1! px-3!"
:disabled="!isDirty || saving"
@click="discardChanges"
>
@@ -49,7 +56,7 @@
</button>
<button
type="button"
- class="primary-chip !py-1 !px-3"
+ class="primary-chip py-1! px-3!"
:disabled="!isDirty || saving"
@click="saveConfig"
>
@@ -82,7 +89,7 @@
</div>
<button
type="button"
- class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-sm disabled:opacity-50"
+ class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-xs disabled:opacity-50"
:disabled="reloadingRns"
:class="reloadingRns ? '' : 'animate-pulse motion-reduce:animate-none'"
@click="reloadRns"
@@ -114,7 +121,7 @@
autocomplete="off"
autocorrect="off"
:placeholder="loading ? $t('tools.reticulum_config_editor.loading') : ''"
- class="w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-xs sm:text-sm resize-none focus:outline-none min-h-[420px] sm:min-h-[60vh]"
+ class="w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-xs sm:text-sm resize-none focus:outline-hidden min-h-[420px] sm:min-h-[60vh]"
@keydown.tab.prevent="insertTab"
></textarea>
</div>
diff --git a/meshchatx/src/frontend/components/tools/SieveFiltersPage.vue b/meshchatx/src/frontend/components/tools/SieveFiltersPage.vue
index 437d0b22..be4c760c 100644
--- a/meshchatx/src/frontend/components/tools/SieveFiltersPage.vue
+++ b/meshchatx/src/frontend/components/tools/SieveFiltersPage.vue
@@ -30,7 +30,7 @@
class="inline-flex items-center gap-2 text-sm text-violet-600 dark:text-violet-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
- {{ $t("tools.sieve_filters.back_tools") }}
+ {{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
@@ -76,7 +76,7 @@
<input
v-model="rule.enabled"
type="checkbox"
- class="rounded border-gray-300"
+ class="rounded-sm border-gray-300"
/>
{{ $t("tools.sieve_filters.enabled") }}
</label>
@@ -152,7 +152,7 @@
<input
v-model="rule.match_peer_fields"
type="checkbox"
- class="rounded border-gray-300"
+ class="rounded-sm border-gray-300"
@change="onMatchTargetsChange(rule)"
/>
{{ $t("tools.sieve_filters.match_peer_fields") }}
@@ -161,7 +161,7 @@
<input
v-model="rule.match_message"
type="checkbox"
- class="rounded border-gray-300"
+ class="rounded-sm border-gray-300"
@change="onMatchTargetsChange(rule)"
/>
{{ $t("tools.sieve_filters.match_message") }}
diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue
index a2e5d7d4..92c07c94 100644
--- a/meshchatx/src/frontend/components/tools/ToolsPage.vue
+++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue
@@ -26,7 +26,7 @@
v-model="searchQuery"
type="text"
:placeholder="$t('common.search')"
- class="w-full pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-sm"
+ class="w-full pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-hidden focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-sm"
/>
<button
v-if="searchQuery"
@@ -41,7 +41,7 @@
</div>
</div>
- <div class="p-4 md:p-6 xl:p-8 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-[96rem] mx-auto">
+ <div class="p-4 md:p-6 xl:p-8 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-384 mx-auto">
<div
class="rounded-lg overflow-hidden border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
>
@@ -75,7 +75,7 @@
<div class="tool-card__title">{{ tool.title }}</div>
<span
v-if="tool.comingSoon"
- class="px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gray-100 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 rounded border border-gray-200 dark:border-zinc-700"
+ class="px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gray-100 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 rounded-sm border border-gray-200 dark:border-zinc-700"
>
Soon
</span>
@@ -340,8 +340,9 @@ export default {
</script>
<style scoped>
+@reference "../../style.css";
.tool-row {
- @apply flex items-start sm:items-center gap-3 sm:gap-4 px-4 py-3.5 min-h-[4.25rem] transition-colors;
+ @apply flex items-start sm:items-center gap-3 sm:gap-4 px-4 py-3.5 min-h-17 transition-colors;
@apply hover:bg-gray-50 dark:hover:bg-zinc-900/80 active:bg-gray-100 dark:active:bg-zinc-800/80;
}
.tool-card__icon {
diff --git a/meshchatx/src/frontend/components/translator/TranslatorPage.vue b/meshchatx/src/frontend/components/translator/TranslatorPage.vue
index 7a6e577e..1daaadce 100644
--- a/meshchatx/src/frontend/components/translator/TranslatorPage.vue
+++ b/meshchatx/src/frontend/components/translator/TranslatorPage.vue
@@ -17,33 +17,56 @@
</div>
</div>
- <div
- v-if="config && !config.translator_enabled"
- class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200/50 dark:border-amber-800/30"
- >
- <div class="flex items-start gap-3">
- <div class="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
- <MaterialDesignIcon
- icon-name="alert-outline"
- class="size-5 text-amber-600 dark:text-amber-400"
- />
- </div>
- <div class="flex-1 text-sm text-amber-800 dark:text-amber-200">
- <p class="font-bold mb-1">Translator is Disabled</p>
- <p class="opacity-90">
- The translation service is currently disabled in your settings. You can enable it
- under
- <RouterLink :to="{ name: 'settings' }" class="font-bold underline"
- >Settings</RouterLink
- >.
- </p>
- </div>
- </div>
+ <div v-if="config" class="space-y-3">
+ <div class="text-sm font-semibold text-gray-800 dark:text-gray-200">Translation backends</div>
+ <label
+ v-if="hasArgos"
+ class="flex items-start gap-3 cursor-pointer p-2 rounded-lg hover:bg-slate-100/80 dark:hover:bg-zinc-900/40"
+ >
+ <Toggle
+ :model-value="config.translator_argos_enabled"
+ @update:model-value="onArgosEnabledChange"
+ />
+ <span>
+ <span class="block text-sm font-medium text-gray-900 dark:text-white"
+ >Argos Translate (local)</span
+ >
+ <span class="text-xs text-gray-500 dark:text-gray-400"
+ >Local packages when Argos is installed. Load languages to refresh this list.</span
+ >
+ </span>
+ </label>
+ <label
+ v-if="libreClientAvailable"
+ class="flex items-start gap-3 cursor-pointer p-2 rounded-lg hover:bg-slate-100/80 dark:hover:bg-zinc-900/40"
+ >
+ <Toggle
+ :model-value="config.translator_libretranslate_enabled"
+ @update:model-value="onLibreEnabledChange"
+ />
+ <span>
+ <span class="block text-sm font-medium text-gray-900 dark:text-white"
+ >LibreTranslate (HTTP)</span
+ >
+ <span class="text-xs text-gray-500 dark:text-gray-400"
+ >Set the base URL below, then enable. Use Refresh languages after the server is
+ up.</span
+ >
+ </span>
+ </label>
+ <p
+ v-if="libreClientAvailable && !libretranslateReachable"
+ class="text-xs text-amber-800/90 dark:text-amber-200/80 px-2 -mt-1"
+ >
+ No response from the LibreTranslate URL yet. Check the address, start the service, and tap
+ Refresh languages.
+ </p>
</div>
<div class="border-b border-gray-200 dark:border-zinc-700">
- <div class="flex -mb-px">
+ <div v-if="hasArgos || libreClientAvailable" class="flex -mb-px">
<button
+ v-if="hasArgos"
type="button"
class="px-4 py-2 text-sm font-semibold border-b-2 transition-colors"
:class="
@@ -56,6 +79,7 @@
Argos Translate
</button>
<button
+ v-if="libreClientAvailable"
type="button"
class="px-4 py-2 text-sm font-semibold border-b-2 transition-colors"
:class="
@@ -139,7 +163,7 @@
</button>
</div>
<div
- class="bg-amber-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all"
+ class="bg-amber-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all"
>
pip install argostranslate
</div>
@@ -158,7 +182,7 @@
</button>
</div>
<div
- class="bg-amber-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all"
+ class="bg-amber-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all"
>
pipx install argostranslate
</div>
@@ -220,7 +244,7 @@
Install All
</button>
<div
- class="bg-blue-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all flex-1"
+ class="bg-blue-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all flex-1"
>
argospm install translate
</div>
@@ -240,7 +264,7 @@
</button>
</div>
<div
- class="bg-blue-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all"
+ class="bg-blue-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all"
>
argospm install translate-en_de
</div>
@@ -330,7 +354,7 @@
<span
v-for="lang in filteredLanguages"
:key="lang.code"
- class="px-2 py-1 rounded text-xs bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300"
+ class="px-2 py-1 rounded-sm text-xs bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300"
>
{{ lang.name }} ({{ lang.code }})
<span class="text-gray-500 dark:text-gray-500">- {{ lang.source }}</span>
@@ -366,11 +390,13 @@
import DialogUtils from "../../js/DialogUtils";
import ToastUtils from "../../js/ToastUtils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
+import Toggle from "../forms/Toggle.vue";
export default {
name: "TranslatorPage",
components: {
MaterialDesignIcon,
+ Toggle,
},
data() {
return {
@@ -381,7 +407,10 @@ export default {
inputText: "",
translationMode: "argos",
libretranslateUrl: "http://localhost:5000",
- hasArgos: true,
+ hasArgos: false,
+ libreClientAvailable: false,
+ libretranslateReachable: false,
+ saveLibreUrlTimer: null,
isTranslating: false,
isInstallingLanguages: false,
translationResult: null,
@@ -390,8 +419,12 @@ export default {
},
computed: {
canTranslate() {
+ const a = this.config?.translator_argos_enabled;
+ const l = this.config?.translator_libretranslate_enabled;
+ const argosOk = this.translationMode === "argos" && a;
+ const libreOk = this.translationMode === "libretranslate" && l;
return (
- this.config?.translator_enabled &&
+ (argosOk || libreOk) &&
this.inputText.trim().length > 0 &&
this.targetLang &&
this.targetLang !== this.sourceLang
@@ -421,6 +454,12 @@ export default {
this.loadLanguages();
},
libretranslateUrl() {
+ if (this.saveLibreUrlTimer) {
+ clearTimeout(this.saveLibreUrlTimer);
+ }
+ this.saveLibreUrlTimer = setTimeout(() => {
+ this.persistLibreUrl();
+ }, 800);
if (this.translationMode === "libretranslate") {
this.loadLanguages();
}
@@ -434,15 +473,67 @@ export default {
try {
const response = await window.api.get("/api/v1/config");
this.config = response.data.config;
- if (this.config.translator_enabled) {
- this.loadLanguages();
+ if (this.config?.libretranslate_url) {
+ this.libretranslateUrl = this.config.libretranslate_url;
}
+ this.loadLanguages();
} catch (e) {
console.log(e);
}
},
+ syncTranslationModeFromBackends() {
+ const canArgos = this.hasArgos;
+ const canLibre = this.libreClientAvailable;
+ if (this.translationMode === "argos" && !canArgos && canLibre) {
+ this.translationMode = "libretranslate";
+ this.sourceLang = "auto";
+ } else if (this.translationMode === "libretranslate" && !canLibre && canArgos) {
+ this.translationMode = "argos";
+ if (this.sourceLang === "auto") {
+ this.sourceLang = "";
+ }
+ } else if (canArgos && !canLibre) {
+ this.translationMode = "argos";
+ } else if (!canArgos && canLibre) {
+ this.translationMode = "libretranslate";
+ if (!this.sourceLang) {
+ this.sourceLang = "auto";
+ }
+ }
+ },
+ async onArgosEnabledChange(value) {
+ if (this.config) {
+ this.config.translator_argos_enabled = value;
+ }
+ try {
+ await window.api.patch("/api/v1/config", { translator_argos_enabled: value });
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ async onLibreEnabledChange(value) {
+ if (this.config) {
+ this.config.translator_libretranslate_enabled = value;
+ }
+ try {
+ await window.api.patch("/api/v1/config", { translator_libretranslate_enabled: value });
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ async persistLibreUrl() {
+ if (!this.config || this.libretranslateUrl === (this.config.libretranslate_url || "")) {
+ return;
+ }
+ try {
+ await window.api.patch("/api/v1/config", { libretranslate_url: this.libretranslateUrl });
+ this.config.libretranslate_url = this.libretranslateUrl;
+ } catch (e) {
+ console.error(e);
+ }
+ },
async loadLanguages() {
- if (this.config && !this.config.translator_enabled) {
+ if (!this.config) {
return;
}
try {
@@ -453,6 +544,9 @@ export default {
const response = await window.api.get("/api/v1/translator/languages", { params });
this.languages = response.data.languages || [];
this.hasArgos = response.data.has_argos;
+ this.libreClientAvailable = Boolean(response.data.libre_client_available);
+ this.libretranslateReachable = Boolean(response.data.libretranslate_reachable);
+ this.syncTranslationModeFromBackends();
} catch (e) {
console.error(e);
DialogUtils.alert(this.$t("translator.failed_load_languages"));
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────